@celestia-island/hikari 0.40.35 → 0.40.36

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.40.35",
3
+ "version": "0.40.36",
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",
package/src/DemoApp.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  HProgressBar, HProgressRing, HGaugeRing,
10
10
  HInput, HSearchInput, HNumberInput, HPasswordInput, HTextarea,
11
11
  HFileField,
12
+ HFilePickerField,
12
13
  type PickedFile, type RemoteFsAdapter,
13
14
  HCheckbox, HSwitch, HRadio, HSelect, HSelectPanel,
14
15
  HLocalizedInput,
@@ -21,7 +22,7 @@ import {
21
22
  type TrendPen, type MinimapBox,
22
23
  } from "@celestia-island/hikari";
23
24
 
24
- const totalComponents = 66;
25
+ const totalComponents = 67;
25
26
 
26
27
  const icons = ["home", "settings", "user", "search", "bell", "heart", "star", "mail", "download", "upload", "trash", "edit", "plus", "check", "x"];
27
28
 
@@ -102,6 +103,12 @@ export default defineComponent({
102
103
  const form = ref({ text: "", search: "", number: 0, password: "", textarea: "" });
103
104
  const localFiles = ref<PickedFile[]>([]);
104
105
  const remoteFiles = ref<PickedFile[]>([]);
106
+ const hookPath = ref("");
107
+ const remotePath = ref("/data");
108
+ // Stands in for a host-opened OS dialog (the Tauri2/Electron case —
109
+ // the window lives outside the webview, so the field takes a hook).
110
+ const fakeHostPicker = async (): Promise<string | null> =>
111
+ Promise.resolve("/opt/acme/install");
105
112
  // In-memory remote FS so the remote picker demo browses something real.
106
113
  const demoRemoteFs: RemoteFsAdapter = (() => {
107
114
  const tree: Record<string, { name: string; kind: "file" | "dir"; size?: number }[]> = {
@@ -317,6 +324,27 @@ export default defineComponent({
317
324
  ) : null}
318
325
  </section>
319
326
 
327
+ <section>
328
+ <h2>HFilePickerField</h2>
329
+ <div class="form-grid">
330
+ <HFilePickerField
331
+ modelValue={hookPath.value}
332
+ onUpdate:modelValue={(v: string) => (hookPath.value = v)}
333
+ label="Install location (host hook)"
334
+ hint="A host-opened OS dialog — e.g. the Tauri2 dialog plugin."
335
+ pick={fakeHostPicker}
336
+ />
337
+ <HFilePickerField
338
+ modelValue={remotePath.value}
339
+ onUpdate:modelValue={(v: string) => (remotePath.value = v)}
340
+ label="Remote folder (in-app picker)"
341
+ backend="remote"
342
+ adapter={demoRemoteFs}
343
+ quickLinks={[{ label: "Root", path: "/" }, { label: "Data", path: "/data" }]}
344
+ />
345
+ </div>
346
+ </section>
347
+
320
348
  <section>
321
349
  <h2>HCheckbox / HSwitch / HRadio</h2>
322
350
  <div class="row">
@@ -52,7 +52,6 @@ export default defineComponent({
52
52
 
53
53
  return () => {
54
54
  const inputType = props.type === "radio" ? "radio" : "checkbox";
55
- const isChecked = props.modelValue === true || props.modelValue === null;
56
55
 
57
56
  return (
58
57
  <label
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, it } from "vitest";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { createApp, defineComponent, h } from "vue";
3
3
 
4
4
  import HkEmptyState from "./HkEmptyState";
@@ -56,6 +56,26 @@ describe("HkEmptyState", () => {
56
56
  expect(btn.textContent).toBe("Retry");
57
57
  });
58
58
 
59
+ it("accepts functional icon components without a prop-type warning", () => {
60
+ // lucide-vue-next icons are plain functions; the Object-only runtime
61
+ // check rejected them with a dev warning at every chest call site
62
+ // (fixed 2026-09-09: [Object, Function]).
63
+ const warnings: string[] = [];
64
+ const spy = vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
65
+ warnings.push(args.join(" "));
66
+ });
67
+ try {
68
+ // Shape-mirrors a lucide icon: a functional component accepting
69
+ // props (size/aria) and rendering a node.
70
+ const lucideLike = ((_props: unknown) => h("i", { class: "fn-icon" })) as never;
71
+ const c = mount(h(HkEmptyState, { title: "No items", icon: lucideLike }));
72
+ expect(c.querySelector(".hk-empty-icon .fn-icon")).not.toBeNull();
73
+ expect(warnings.filter((w) => w.includes("Invalid prop"))).toEqual([]);
74
+ } finally {
75
+ spy.mockRestore();
76
+ }
77
+ });
78
+
59
79
  it("renders the icon prop component in the icon well", () => {
60
80
  const c = mount(h(HkEmptyState, { title: "No items", icon: StubIcon }));
61
81
  expect(c.querySelector(".hk-empty-icon .stub-empty-icon")).not.toBeNull();
@@ -11,7 +11,7 @@ export default defineComponent({
11
11
  title: { type: String, default: undefined },
12
12
  description: { type: String, default: undefined },
13
13
  /** Icon component (lucide-vue-next style); rendered via h() at size 32. */
14
- icon: { type: Object as PropType<Component>, default: undefined },
14
+ icon: { type: [Object, Function] as PropType<Component>, default: undefined },
15
15
  /** Loading variant: only a centered spinner, exposed as a status region. */
16
16
  loading: { type: Boolean, default: false },
17
17
  /** page = bounded, centered card on desktop; fill = stretch to the host. */
@@ -71,6 +71,7 @@ interface MountOptions {
71
71
  multiple?: boolean;
72
72
  quickLinks?: Array<{ label: string; path: string }>;
73
73
  initialPath?: string;
74
+ pickDirectory?: boolean;
74
75
  }
75
76
 
76
77
  function mountDialog(adapter: RemoteFsAdapter, opts: MountOptions = {}) {
@@ -91,6 +92,7 @@ function mountDialog(adapter: RemoteFsAdapter, opts: MountOptions = {}) {
91
92
  multiple: opts.multiple ?? false,
92
93
  quickLinks: opts.quickLinks ?? [],
93
94
  initialPath: opts.initialPath ?? "/",
95
+ pickDirectory: opts.pickDirectory ?? false,
94
96
  "onUpdate:modelValue": (v: boolean) => {
95
97
  openEvents.push(v);
96
98
  open.value = v;
@@ -446,3 +448,44 @@ describe("HkFileBrowserDialog load states", () => {
446
448
  expect(rowNames()).toEqual(["ok.txt"]);
447
449
  });
448
450
  });
451
+
452
+ describe("HkFileBrowserDialog directory mode", () => {
453
+ it("confirms the listed folder without needing a selection", async () => {
454
+ const adapter = makeAdapter();
455
+ const { confirmed, openEvents } = mountDialog(adapter, { pickDirectory: true });
456
+ await flush();
457
+
458
+ // Nothing is selected, yet confirm is live: the current folder IS
459
+ // the choice.
460
+ const confirmBtn = q<HTMLButtonElement>(".hk-file-browser-confirm");
461
+ expect(confirmBtn.disabled).toBe(false);
462
+ expect(confirmBtn.textContent).toContain("Choose this folder");
463
+ confirmBtn.click();
464
+ await flush();
465
+ expect(confirmed).toEqual([[{ name: "/", path: "/" }]]);
466
+ expect(openEvents).toContain(false);
467
+ });
468
+
469
+ it("navigates into subfolders and hands back that folder's path", async () => {
470
+ const adapter = makeAdapter();
471
+ const { confirmed } = mountDialog(adapter, { pickDirectory: true });
472
+ await flush();
473
+
474
+ await clickRow("reports");
475
+ expect(adapter.list).toHaveBeenLastCalledWith("/reports");
476
+ const confirmBtn = q<HTMLButtonElement>(".hk-file-browser-confirm");
477
+ confirmBtn.click();
478
+ await flush();
479
+ expect(confirmed).toEqual([[{ name: "reports", path: "/reports" }]]);
480
+ });
481
+
482
+ it("never selects file rows while picking a folder", async () => {
483
+ const adapter = makeAdapter();
484
+ mountDialog(adapter, { pickDirectory: true });
485
+ await flush();
486
+
487
+ await clickRow("data.csv");
488
+ const rows = qa<HTMLButtonElement>(".hk-file-browser-row");
489
+ expect(rows.every((r) => r.dataset.selected === undefined)).toBe(true);
490
+ });
491
+ });
@@ -112,6 +112,11 @@ export default defineComponent({
112
112
  type: Array as PropType<FileQuickLink[]>,
113
113
  default: () => [],
114
114
  },
115
+ /**
116
+ * Directory-picking mode: confirm chooses the folder currently
117
+ * listed (entries only navigate), instead of selecting files.
118
+ */
119
+ pickDirectory: { type: Boolean, default: false },
115
120
  confirmLabel: { type: String, default: undefined },
116
121
  cancelLabel: { type: String, default: undefined },
117
122
  },
@@ -290,6 +295,8 @@ export default defineComponent({
290
295
  navigate(joinPath(currentPath.value, entry.name));
291
296
  return;
292
297
  }
298
+ // Directory-picking mode only navigates; files are inert rows.
299
+ if (props.pickDirectory) return;
293
300
  const next = new Set(selected.value);
294
301
  if (props.multiple) {
295
302
  if (next.has(entry.name)) next.delete(entry.name);
@@ -306,7 +313,7 @@ export default defineComponent({
306
313
  /** Double-click activates: the file confirms immediately (and alone,
307
314
  * even in multiple mode — a double-click names ONE file). */
308
315
  function onEntryActivate(entry: RemoteFileEntry) {
309
- if (entry.kind !== "file") return;
316
+ if (entry.kind !== "file" || props.pickDirectory) return;
310
317
  selected.value = new Set([entry.name]);
311
318
  confirmSelection();
312
319
  }
@@ -394,6 +401,15 @@ export default defineComponent({
394
401
  }
395
402
 
396
403
  function confirmSelection() {
404
+ // Directory mode: the folder currently listed IS the choice —
405
+ // the pathbar has been showing it the whole time.
406
+ if (props.pickDirectory) {
407
+ const path = currentPath.value;
408
+ const name = path.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? "/";
409
+ emit("confirm", [{ name, path }]);
410
+ close();
411
+ return;
412
+ }
397
413
  if (selected.value.size === 0) return;
398
414
  const files: ConfirmedFile[] = [];
399
415
  for (const name of selected.value) {
@@ -770,10 +786,13 @@ export default defineComponent({
770
786
  variant="primary"
771
787
  size="md"
772
788
  class="hk-file-browser-confirm"
773
- disabled={selected.value.size === 0}
789
+ disabled={!props.pickDirectory && selected.value.size === 0}
774
790
  onClick={confirmSelection}
775
791
  >
776
- {props.confirmLabel ?? t("hikari::filePicker.confirm", "Confirm")}
792
+ {props.confirmLabel ??
793
+ (props.pickDirectory
794
+ ? t("hikari::filePicker.chooseFolder", "Choose this folder")
795
+ : t("hikari::filePicker.confirm", "Confirm"))}
777
796
  </HButton>
778
797
  </div>
779
798
  </div>
@@ -783,7 +802,12 @@ export default defineComponent({
783
802
  return () => (
784
803
  <HModal
785
804
  modelValue={props.modelValue}
786
- title={props.title ?? t("hikari::filePicker.title", "Browse files")}
805
+ title={
806
+ props.title ??
807
+ (props.pickDirectory
808
+ ? t("hikari::filePicker.titleFolder", "Browse folders")
809
+ : t("hikari::filePicker.title", "Browse files"))
810
+ }
787
811
  width="56rem"
788
812
  onUpdate:modelValue={(v: boolean) => emit("update:modelValue", v)}
789
813
  >
@@ -0,0 +1,93 @@
1
+ .hk-file-picker-field {
2
+ display: block;
3
+ }
4
+
5
+ .hk-file-picker-label {
6
+ display: block;
7
+ font-size: var(--text-xs);
8
+ font-weight: 600;
9
+ letter-spacing: 0.02em;
10
+ margin-bottom: var(--space-4);
11
+ color: rgb(var(--color-text) / var(--hk-field-label-alpha, 72%));
12
+ }
13
+
14
+ .hk-file-picker-box {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: var(--space-6);
18
+ padding: var(--space-4) var(--space-6);
19
+ min-height: 2.5rem;
20
+ background: color-mix(in srgb, rgb(var(--color-surface)) 55%, transparent);
21
+ border: 1px solid color-mix(in srgb, rgb(var(--color-text)) 14%, transparent);
22
+ border-radius: var(--radius-sm);
23
+ transition:
24
+ border-color var(--duration-normal) var(--ease-standard),
25
+ background-color var(--duration-normal) var(--ease-standard);
26
+
27
+ &:focus-within {
28
+ border-color: color-mix(in srgb, rgb(var(--color-text)) 38%, transparent);
29
+ }
30
+
31
+ &[data-error] {
32
+ border-color: var(--hi-color-danger);
33
+ }
34
+
35
+ &[data-disabled] {
36
+ opacity: 0.55;
37
+ pointer-events: none;
38
+ }
39
+ }
40
+
41
+ /* The shittim-chest file-picker badge: a primary-tinted rounded square
42
+ carrying the folder glyph, anchoring the field's affordance. */
43
+ .hk-file-picker-badge {
44
+ flex-shrink: 0;
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ width: 1.75rem;
49
+ height: 1.75rem;
50
+ border-radius: var(--radius-xs);
51
+ background: color-mix(in srgb, var(--hi-color-primary) 14%, transparent);
52
+ color: var(--hi-color-primary);
53
+ pointer-events: none;
54
+ }
55
+
56
+ .hk-file-picker-input {
57
+ flex: 1 1 auto;
58
+ min-width: 0;
59
+ padding: 0;
60
+ background: transparent;
61
+ border: 0;
62
+ outline: none;
63
+ color: rgb(var(--color-text));
64
+ font-size: var(--text-sm);
65
+ font-family: inherit;
66
+
67
+ &::placeholder {
68
+ color: rgb(var(--color-text) / 55%);
69
+ }
70
+
71
+ /* Paths are code — mono by default, opt out with :mono="false". */
72
+ .hk-file-picker-box[data-mono] & {
73
+ font-family: var(--font-mono, ui-monospace, monospace);
74
+ }
75
+
76
+ &:disabled {
77
+ cursor: default;
78
+ }
79
+ }
80
+
81
+ .hk-file-picker-browse {
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .hk-file-picker-hint {
86
+ margin: var(--space-4) 0 0;
87
+ font-size: var(--text-xs);
88
+ color: rgb(var(--color-text) / 60%);
89
+
90
+ &[data-error] {
91
+ color: var(--hi-color-danger);
92
+ }
93
+ }
@@ -0,0 +1,194 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createApp, defineComponent, h, nextTick, ref } from "vue";
3
+
4
+ import HkFilePickerField from "./HkFilePickerField";
5
+
6
+ vi.mock("./HkFileBrowserDialog", () => {
7
+ return {
8
+ default: defineComponent({
9
+ name: "HkFileBrowserDialogStub",
10
+ props: {
11
+ modelValue: { type: Boolean, default: false },
12
+ pickDirectory: { type: Boolean, default: false },
13
+ },
14
+ emits: ["update:modelValue", "confirm"],
15
+ setup(props, { emit }) {
16
+ return () =>
17
+ props.modelValue
18
+ ? h(
19
+ "div",
20
+ { class: "picker-dialog-stub" },
21
+ h("button", {
22
+ class: "picker-dialog-stub-confirm",
23
+ onClick: () => {
24
+ // Directory mode hands back the listed folder itself.
25
+ emit(
26
+ "confirm",
27
+ props.pickDirectory
28
+ ? [{ name: "data", path: "/srv/data" }]
29
+ : [{ name: "a.csv", path: "/srv/data/a.csv" }],
30
+ );
31
+ emit("update:modelValue", false);
32
+ },
33
+ }),
34
+ )
35
+ : null;
36
+ },
37
+ }),
38
+ };
39
+ });
40
+
41
+ const mounts: ReturnType<typeof createApp>[] = [];
42
+ const containers: HTMLElement[] = [];
43
+
44
+ afterEach(() => {
45
+ for (const app of mounts.splice(0)) app.unmount();
46
+ for (const el of containers.splice(0)) el.remove();
47
+ });
48
+
49
+ interface FieldHandle {
50
+ input: HTMLInputElement;
51
+ browse: HTMLButtonElement;
52
+ model: { value: string };
53
+ }
54
+
55
+ async function mountField(props: Record<string, unknown> = {}): Promise<FieldHandle> {
56
+ const container = document.createElement("div");
57
+ document.body.appendChild(container);
58
+ containers.push(container);
59
+
60
+ const model = ref("");
61
+ // An initial value seeds the ref; the wrapper keeps driving the prop.
62
+ if (typeof props.modelValue === "string") {
63
+ model.value = props.modelValue;
64
+ delete props.modelValue;
65
+ }
66
+ const Wrapper = defineComponent({
67
+ setup() {
68
+ return () =>
69
+ h(HkFilePickerField, {
70
+ modelValue: model.value,
71
+ "onUpdate:modelValue": (v: string) => {
72
+ model.value = v;
73
+ },
74
+ ...props,
75
+ });
76
+ },
77
+ });
78
+ const app = createApp(Wrapper);
79
+ mounts.push(app);
80
+ app.mount(container);
81
+ await nextTick();
82
+
83
+ return {
84
+ input: container.querySelector<HTMLInputElement>(".hk-file-picker-input")!,
85
+ browse: container.querySelector<HTMLButtonElement>(".hk-file-picker-browse")!,
86
+ model,
87
+ };
88
+ }
89
+
90
+ describe("HkFilePickerField hook backend", () => {
91
+ it("adopts the hook-resolved path and leaves dismissals untouched", async () => {
92
+ const pick = vi.fn(async () => "/opt/shun/target");
93
+ const field = await mountField({ pick });
94
+ expect(field.input.placeholder).toContain("folder");
95
+ await field.browse.click();
96
+ await nextTick();
97
+ expect(pick).toHaveBeenCalledTimes(1);
98
+ expect(field.model.value).toBe("/opt/shun/target");
99
+ expect(field.input.value).toBe("/opt/shun/target");
100
+
101
+ // A dismissed picker (null) keeps the current value.
102
+ const dismissed = vi.fn(async () => null);
103
+ const field2 = await mountField({ pick: dismissed, modelValue: "/keep" });
104
+ await field2.browse.click();
105
+ await nextTick();
106
+ expect(field2.model.value).toBe("/keep");
107
+ });
108
+
109
+ it("disables the browse button when the hook backend has no hook", async () => {
110
+ const field = await mountField({ backend: "hook" });
111
+ expect(field.browse.disabled).toBe(true);
112
+ });
113
+
114
+ it("does not offer browsing at all when disabled", async () => {
115
+ const pick = vi.fn(async () => "/x");
116
+ const field = await mountField({ pick, disabled: true });
117
+ expect(field.browse.disabled).toBe(true);
118
+ field.browse.click();
119
+ expect(pick).not.toHaveBeenCalled();
120
+ });
121
+ });
122
+
123
+ describe("HkFilePickerField remote backend", () => {
124
+ it("auto-selects remote from the adapter and adopts the confirmed folder", async () => {
125
+ const adapter = { list: vi.fn(async (path: string) => ({ path, entries: [] })) };
126
+ const field = await mountField({ adapter });
127
+ // The dialog renders inside the field's mount, not the stub's body.
128
+ await field.browse.click();
129
+ await nextTick();
130
+ const confirm = field.browse
131
+ .closest(".hk-file-picker-field")!
132
+ .querySelector<HTMLButtonElement>(".picker-dialog-stub-confirm")!;
133
+ expect(confirm).toBeTruthy();
134
+ await confirm.click();
135
+ await nextTick();
136
+ expect(field.model.value).toBe("/srv/data");
137
+ expect(
138
+ field.browse
139
+ .closest(".hk-file-picker-field")!
140
+ .querySelector(".picker-dialog-stub"),
141
+ ).toBeNull();
142
+ });
143
+ });
144
+
145
+ describe("HkFilePickerField native backend", () => {
146
+ it("resolves the leaf name through the FS Access directory picker", async () => {
147
+ const showDirectoryPicker = vi.fn(async () => ({ name: "chosen-dir" }));
148
+ Object.defineProperty(window, "showDirectoryPicker", {
149
+ configurable: true,
150
+ value: showDirectoryPicker,
151
+ });
152
+ const field = await mountField({ backend: "native" });
153
+ await field.browse.click();
154
+ await nextTick();
155
+ expect(field.model.value).toBe("chosen-dir");
156
+ Object.defineProperty(window, "showDirectoryPicker", {
157
+ configurable: true,
158
+ value: undefined,
159
+ });
160
+ });
161
+
162
+ it("swallows a cancelled native picker without touching the value", async () => {
163
+ const showDirectoryPicker = vi.fn(async () => {
164
+ throw new DOMException("cancel", "AbortError");
165
+ });
166
+ Object.defineProperty(window, "showDirectoryPicker", {
167
+ configurable: true,
168
+ value: showDirectoryPicker,
169
+ });
170
+ const field = await mountField({ backend: "native", modelValue: "/keep" });
171
+ await field.browse.click();
172
+ await nextTick();
173
+ expect(field.model.value).toBe("/keep");
174
+ Object.defineProperty(window, "showDirectoryPicker", {
175
+ configurable: true,
176
+ value: undefined,
177
+ });
178
+ });
179
+ });
180
+
181
+ describe("HkFilePickerField typing", () => {
182
+ it("emits update:modelValue on input", async () => {
183
+ const field = await mountField();
184
+ field.input.value = "D:\\Games\\Demo";
185
+ field.input.dispatchEvent(new Event("input"));
186
+ await nextTick();
187
+ expect(field.model.value).toBe("D:\\Games\\Demo");
188
+ });
189
+
190
+ it("shows the file placeholder when directory is off", async () => {
191
+ const field = await mountField({ directory: false });
192
+ expect(field.input.placeholder).toContain("file");
193
+ });
194
+ });
@@ -0,0 +1,212 @@
1
+ import { computed, defineComponent, ref, useId, type PropType } from "vue";
2
+
3
+ import { useI18n } from "../i18n/context";
4
+ import HButton from "./HkButton";
5
+ import HkFileBrowserDialog from "./HkFileBrowserDialog";
6
+ import type {
7
+ FilePickerBackend,
8
+ FilePickerHook,
9
+ FileQuickLink,
10
+ RemoteFsAdapter,
11
+ } from "./filePicker";
12
+ import "./HkFilePickerField.scss";
13
+
14
+ /**
15
+ * HkFilePickerField — the path-valued sibling of HkFileField. Where the
16
+ * file field hands back file CONTENT (File objects / remote entries),
17
+ * this one holds a PATH STRING the user can also type by hand: a
18
+ * folder badge, an editable input, and a browse button pinned right.
19
+ *
20
+ * Three picker backends, all addressable through `backend`:
21
+ * - `hook`: the host opens its own picker (`pick`) and resolves the
22
+ * chosen path. The canonical case is a Tauri2/Electron host whose
23
+ * native dialog is a real OS window OUTSIDE the webview — neither
24
+ * of the other backends can stand in for it.
25
+ * - `remote`: HkFileBrowserDialog driven by the provided `adapter`
26
+ * (in `directory` mode it picks the folder being listed).
27
+ * - `native`: the browser's own FS Access picker
28
+ * (`showDirectoryPicker` / `showOpenFilePicker`) where the platform
29
+ * supplies one. Opaque handles only ever expose their leaf name —
30
+ * fine for relative targets, useless for absolute ones; installers
31
+ * and friends should reach for the hook instead.
32
+ *
33
+ * `auto` (default) resolves hook → remote → native from whichever
34
+ * props are present, degrading to a purely typed field when none are.
35
+ */
36
+ export default defineComponent({
37
+ name: "HkFilePickerField",
38
+ props: {
39
+ modelValue: { type: String, default: "" },
40
+ backend: {
41
+ type: String as () => FilePickerBackend,
42
+ default: "auto",
43
+ },
44
+ /** Pick a directory (default) or a file. */
45
+ directory: { type: Boolean, default: true },
46
+ /** The `hook` backend: open a host picker, resolve the path or null. */
47
+ pick: { type: Function as PropType<FilePickerHook>, default: undefined },
48
+ // ── remote backend ──
49
+ adapter: { type: Object as () => RemoteFsAdapter, default: undefined },
50
+ quickLinks: { type: Array as () => FileQuickLink[], default: () => [] },
51
+ initialPath: { type: String, default: "/" },
52
+ dialogTitle: { type: String, default: undefined },
53
+ // ── field chrome ──
54
+ label: { type: String, default: undefined },
55
+ placeholder: { type: String, default: undefined },
56
+ hint: { type: String, default: undefined },
57
+ error: { type: String, default: undefined },
58
+ disabled: { type: Boolean, default: false },
59
+ /** Render the path in the mono face (the default — paths are code). */
60
+ mono: { type: Boolean, default: true },
61
+ /** Explicit input id (label `for` binding). */
62
+ id: { type: String, default: undefined },
63
+ },
64
+ emits: {
65
+ "update:modelValue": (_value: string) => true,
66
+ },
67
+ setup(props, { emit }) {
68
+ const { t } = useI18n();
69
+ const generatedId = useId();
70
+ const inputId = computed(() => props.id ?? `${generatedId}-input`);
71
+
72
+ const resolvedBackend = computed<"hook" | "remote" | "native">(() => {
73
+ if (props.backend !== "auto") return props.backend;
74
+ if (props.pick) return "hook";
75
+ if (props.adapter) return "remote";
76
+ return "native";
77
+ });
78
+
79
+ const placeholderText = computed(
80
+ () =>
81
+ props.placeholder ??
82
+ (props.directory
83
+ ? t("hikari::filePicker.fieldPickDirectory", "Choose a folder…")
84
+ : t("hikari::filePicker.fieldPickFile", "Choose a file…")),
85
+ );
86
+
87
+ // Only the remote backend mounts the dialog; every other picker
88
+ // opens outside the component (or nowhere, when typed by hand).
89
+ const dialogOpen = ref(false);
90
+
91
+ /** A backend with its required prop missing cannot open anything. */
92
+ const canBrowse = computed(() => {
93
+ const backend = resolvedBackend.value;
94
+ if (backend === "hook") return typeof props.pick === "function";
95
+ if (backend === "remote") return Boolean(props.adapter);
96
+ return true;
97
+ });
98
+
99
+ async function browse() {
100
+ if (props.disabled) return;
101
+ const backend = resolvedBackend.value;
102
+ if (backend === "hook" && props.pick) {
103
+ const picked = await props.pick();
104
+ if (picked) emit("update:modelValue", picked);
105
+ return;
106
+ }
107
+ if (backend === "remote" && props.adapter) {
108
+ dialogOpen.value = true;
109
+ return;
110
+ }
111
+ if (backend === "native") {
112
+ // FS Access pickers resolve to opaque handles; only the leaf
113
+ // name is readable. Hosts needing absolute paths use the hook.
114
+ const w = window as unknown as {
115
+ showDirectoryPicker?: () => Promise<{ name: string } | undefined>;
116
+ showOpenFilePicker?: () => Promise<{ name: string }[] | undefined>;
117
+ };
118
+ try {
119
+ if (props.directory && w.showDirectoryPicker) {
120
+ const handle = await w.showDirectoryPicker();
121
+ if (handle) emit("update:modelValue", handle.name);
122
+ } else if (!props.directory && w.showOpenFilePicker) {
123
+ const [handle] = (await w.showOpenFilePicker()) ?? [];
124
+ if (handle) emit("update:modelValue", handle.name);
125
+ }
126
+ } catch {
127
+ // A dismissed native picker is not an error worth surfacing.
128
+ }
129
+ }
130
+ }
131
+
132
+ function onInput(event: Event) {
133
+ emit("update:modelValue", (event.target as HTMLInputElement).value);
134
+ }
135
+
136
+ function onDialogConfirm(files: { name: string; path: string }[]) {
137
+ const chosen = files[0];
138
+ if (chosen) emit("update:modelValue", chosen.path);
139
+ dialogOpen.value = false;
140
+ }
141
+
142
+ return () => (
143
+ <div class="hk-file-picker-field">
144
+ {props.label && (
145
+ <label class="hk-file-picker-label" for={inputId.value}>
146
+ {props.label}
147
+ </label>
148
+ )}
149
+ <div
150
+ class="hk-file-picker-box"
151
+ data-disabled={props.disabled || undefined}
152
+ data-error={props.error || undefined}
153
+ data-mono={props.mono || undefined}
154
+ >
155
+ <span class="hk-file-picker-badge" aria-hidden="true">
156
+ <svg
157
+ viewBox="0 0 24 24"
158
+ fill="none"
159
+ stroke="currentColor"
160
+ stroke-width="2"
161
+ stroke-linecap="round"
162
+ stroke-linejoin="round"
163
+ width="18"
164
+ height="18"
165
+ >
166
+ {/* folder-open: the shittim-chest file-picker affordance */}
167
+ <path d="m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.93a2 2 0 0 1 1.66.9l.82 1.2a2 2 0 0 0 1.66.9H18a2 2 0 0 1 2 2v2" />
168
+ </svg>
169
+ </span>
170
+ <input
171
+ id={inputId.value}
172
+ class="hk-file-picker-input"
173
+ type="text"
174
+ value={props.modelValue}
175
+ placeholder={placeholderText.value}
176
+ spellcheck={false}
177
+ disabled={props.disabled}
178
+ onInput={onInput}
179
+ />
180
+ <HButton
181
+ variant="ghost"
182
+ size="sm"
183
+ class="hk-file-picker-browse"
184
+ disabled={props.disabled || !canBrowse.value}
185
+ onClick={browse}
186
+ >
187
+ {t("hikari::filePicker.browse", "Browse…")}
188
+ </HButton>
189
+ </div>
190
+ {props.error ? (
191
+ <p class="hk-file-picker-hint" data-error>{props.error}</p>
192
+ ) : props.hint ? (
193
+ <p class="hk-file-picker-hint">{props.hint}</p>
194
+ ) : null}
195
+ {resolvedBackend.value === "remote" && props.adapter && (
196
+ <HkFileBrowserDialog
197
+ modelValue={dialogOpen.value}
198
+ onUpdate:modelValue={(v: boolean) => {
199
+ dialogOpen.value = v;
200
+ }}
201
+ adapter={props.adapter}
202
+ pickDirectory={props.directory}
203
+ initialPath={props.initialPath}
204
+ quickLinks={props.quickLinks}
205
+ title={props.dialogTitle}
206
+ onConfirm={onDialogConfirm}
207
+ />
208
+ )}
209
+ </div>
210
+ );
211
+ },
212
+ });
@@ -1,4 +1,4 @@
1
- import { computed, defineComponent, type PropType } from "vue";
1
+ import { defineComponent, type PropType } from "vue";
2
2
  import HkLabel from "./HkLabel";
3
3
  import "./HkRadio.scss";
4
4
 
@@ -1,4 +1,4 @@
1
- import { computed, defineComponent, type PropType } from "vue";
1
+ import { defineComponent, type PropType } from "vue";
2
2
  import HkLabel from "./HkLabel";
3
3
  import "./HkSwitch.scss";
4
4
 
@@ -66,3 +66,19 @@ export function acceptExtensions(accept?: string): string[] {
66
66
  .map((part) => part.trim().toLowerCase())
67
67
  .filter((part) => part.startsWith("."));
68
68
  }
69
+
70
+ /**
71
+ * Backend selector for HkFilePickerField — the path-valued picker. All
72
+ * three are addressable so embedders can force one; "auto" resolves
73
+ * hook → remote → native from whichever props are present.
74
+ */
75
+ export type FilePickerBackend = "auto" | "hook" | "remote" | "native";
76
+
77
+ /**
78
+ * The app hook backend: the host opens its own picker and resolves the
79
+ * chosen path (or null for a dismissal). The canonical case is a
80
+ * Tauri2/Electron-style host whose native dialog is a real OS window
81
+ * OUTSIDE the webview — neither the browser picker nor an in-app
82
+ * dialog can stand in for it.
83
+ */
84
+ export type FilePickerHook = () => Promise<string | null>;
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "نسخ الخطأ",
46
46
  "hikari::errorBoundary.retry": "إعادة المحاولة",
47
47
  "hikari::errorBoundary.title": "خطأ في المكوّن",
48
+ "hikari::filePicker.browse": "استعراض…",
48
49
  "hikari::filePicker.cancel": "إلغاء",
50
+ "hikari::filePicker.chooseFolder": "اختيار هذا المجلد",
49
51
  "hikari::filePicker.confirm": "اختيار",
50
52
  "hikari::filePicker.copy": "نسخ",
51
53
  "hikari::filePicker.copyPath": "نسخ المسار",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "اختر ملفات بعيدة…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "اختر ملفًا بعيدًا…",
58
60
  "hikari::filePicker.fieldClear": "مسح التحديد",
61
+ "hikari::filePicker.fieldPickDirectory": "اختيار مجلد…",
62
+ "hikari::filePicker.fieldPickFile": "اختيار ملف…",
59
63
  "hikari::filePicker.filterAll": "كل الملفات",
60
64
  "hikari::filePicker.gridLayout": "عرض الشبكة",
61
65
  "hikari::filePicker.listError": "تعذر عرض هذا المجلد",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "إعادة المحاولة",
76
80
  "hikari::filePicker.size": "الحجم",
77
81
  "hikari::filePicker.title": "اختر الملفات",
82
+ "hikari::filePicker.titleFolder": "استعراض المجلدات",
78
83
  "hikari::imageLightbox.close": "إغلاق",
79
84
  "hikari::imageLightbox.title": "عارض الصور",
80
85
  "hikari::imagePreview.enlarge": "تكبير الصورة",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "Fehler kopieren",
46
46
  "hikari::errorBoundary.retry": "Erneut versuchen",
47
47
  "hikari::errorBoundary.title": "Komponentenfehler",
48
+ "hikari::filePicker.browse": "Durchsuchen…",
48
49
  "hikari::filePicker.cancel": "Abbrechen",
50
+ "hikari::filePicker.chooseFolder": "Diesen Ordner wählen",
49
51
  "hikari::filePicker.confirm": "Auswählen",
50
52
  "hikari::filePicker.copy": "Kopieren",
51
53
  "hikari::filePicker.copyPath": "Pfad kopieren",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "Remote-Dateien auswählen…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "Remote-Datei auswählen…",
58
60
  "hikari::filePicker.fieldClear": "Auswahl löschen",
61
+ "hikari::filePicker.fieldPickDirectory": "Ordner auswählen…",
62
+ "hikari::filePicker.fieldPickFile": "Datei auswählen…",
59
63
  "hikari::filePicker.filterAll": "Alle Dateien",
60
64
  "hikari::filePicker.gridLayout": "Rasteransicht",
61
65
  "hikari::filePicker.listError": "Ordner konnte nicht gelesen werden",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "Wiederholen",
76
80
  "hikari::filePicker.size": "Größe",
77
81
  "hikari::filePicker.title": "Dateien wählen",
82
+ "hikari::filePicker.titleFolder": "Ordner durchsuchen",
78
83
  "hikari::imageLightbox.close": "Schließen",
79
84
  "hikari::imageLightbox.title": "Bildbetrachter",
80
85
  "hikari::imagePreview.enlarge": "Vergrößern",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "Copy Error",
46
46
  "hikari::errorBoundary.retry": "Retry",
47
47
  "hikari::errorBoundary.title": "Component Error",
48
+ "hikari::filePicker.browse": "Browse…",
48
49
  "hikari::filePicker.cancel": "Cancel",
50
+ "hikari::filePicker.chooseFolder": "Choose this folder",
49
51
  "hikari::filePicker.confirm": "Choose",
50
52
  "hikari::filePicker.copy": "Copy",
51
53
  "hikari::filePicker.copyPath": "Copy path",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "Choose remote files…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "Choose a remote file…",
58
60
  "hikari::filePicker.fieldClear": "Clear selection",
61
+ "hikari::filePicker.fieldPickDirectory": "Choose a folder…",
62
+ "hikari::filePicker.fieldPickFile": "Choose a file…",
59
63
  "hikari::filePicker.filterAll": "All files",
60
64
  "hikari::filePicker.gridLayout": "Grid view",
61
65
  "hikari::filePicker.listError": "Couldn't list this folder",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "Retry",
76
80
  "hikari::filePicker.size": "Size",
77
81
  "hikari::filePicker.title": "Choose files",
82
+ "hikari::filePicker.titleFolder": "Browse folders",
78
83
  "hikari::imageLightbox.close": "Close",
79
84
  "hikari::imageLightbox.title": "Image viewer",
80
85
  "hikari::imagePreview.enlarge": "View larger",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "Copiar error",
46
46
  "hikari::errorBoundary.retry": "Reintentar",
47
47
  "hikari::errorBoundary.title": "Error del componente",
48
+ "hikari::filePicker.browse": "Examinar…",
48
49
  "hikari::filePicker.cancel": "Cancelar",
50
+ "hikari::filePicker.chooseFolder": "Elegir esta carpeta",
49
51
  "hikari::filePicker.confirm": "Elegir",
50
52
  "hikari::filePicker.copy": "Copiar",
51
53
  "hikari::filePicker.copyPath": "Copiar ruta",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "Elegir archivos remotos…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "Elegir un archivo remoto…",
58
60
  "hikari::filePicker.fieldClear": "Borrar selección",
61
+ "hikari::filePicker.fieldPickDirectory": "Elegir una carpeta…",
62
+ "hikari::filePicker.fieldPickFile": "Elegir un archivo…",
59
63
  "hikari::filePicker.filterAll": "Todos los archivos",
60
64
  "hikari::filePicker.gridLayout": "Vista de cuadrícula",
61
65
  "hikari::filePicker.listError": "No se pudo listar esta carpeta",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "Reintentar",
76
80
  "hikari::filePicker.size": "Tamaño",
77
81
  "hikari::filePicker.title": "Elegir archivos",
82
+ "hikari::filePicker.titleFolder": "Examinar carpetas",
78
83
  "hikari::imageLightbox.close": "Cerrar",
79
84
  "hikari::imageLightbox.title": "Visor de imágenes",
80
85
  "hikari::imagePreview.enlarge": "Ampliar",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "Copier l'erreur",
46
46
  "hikari::errorBoundary.retry": "Réessayer",
47
47
  "hikari::errorBoundary.title": "Erreur du composant",
48
+ "hikari::filePicker.browse": "Parcourir…",
48
49
  "hikari::filePicker.cancel": "Annuler",
50
+ "hikari::filePicker.chooseFolder": "Choisir ce dossier",
49
51
  "hikari::filePicker.confirm": "Choisir",
50
52
  "hikari::filePicker.copy": "Copier",
51
53
  "hikari::filePicker.copyPath": "Copier le chemin",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "Choisir des fichiers distants…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "Choisir un fichier distant…",
58
60
  "hikari::filePicker.fieldClear": "Effacer la sélection",
61
+ "hikari::filePicker.fieldPickDirectory": "Choisir un dossier…",
62
+ "hikari::filePicker.fieldPickFile": "Choisir un fichier…",
59
63
  "hikari::filePicker.filterAll": "Tous les fichiers",
60
64
  "hikari::filePicker.gridLayout": "Vue grille",
61
65
  "hikari::filePicker.listError": "Impossible de lister ce dossier",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "Réessayer",
76
80
  "hikari::filePicker.size": "Taille",
77
81
  "hikari::filePicker.title": "Choisir des fichiers",
82
+ "hikari::filePicker.titleFolder": "Parcourir les dossiers",
78
83
  "hikari::imageLightbox.close": "Fermer",
79
84
  "hikari::imageLightbox.title": "Visionneuse d'images",
80
85
  "hikari::imagePreview.enlarge": "Agrandir",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "エラーをコピー",
46
46
  "hikari::errorBoundary.retry": "再試行",
47
47
  "hikari::errorBoundary.title": "コンポーネントエラー",
48
+ "hikari::filePicker.browse": "参照…",
48
49
  "hikari::filePicker.cancel": "キャンセル",
50
+ "hikari::filePicker.chooseFolder": "このフォルダーを選択",
49
51
  "hikari::filePicker.confirm": "選択",
50
52
  "hikari::filePicker.copy": "コピー",
51
53
  "hikari::filePicker.copyPath": "パスをコピー",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "複数のリモートファイルを選択…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "リモートファイルを選択…",
58
60
  "hikari::filePicker.fieldClear": "選択を解除",
61
+ "hikari::filePicker.fieldPickDirectory": "フォルダーを選択…",
62
+ "hikari::filePicker.fieldPickFile": "ファイルを選択…",
59
63
  "hikari::filePicker.filterAll": "すべてのファイル",
60
64
  "hikari::filePicker.gridLayout": "グリッド表示",
61
65
  "hikari::filePicker.listError": "このフォルダーを一覧できません",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "再試行",
76
80
  "hikari::filePicker.size": "サイズ",
77
81
  "hikari::filePicker.title": "ファイルを選択",
82
+ "hikari::filePicker.titleFolder": "フォルダーを参照",
78
83
  "hikari::imageLightbox.close": "閉じる",
79
84
  "hikari::imageLightbox.title": "画像ビューア",
80
85
  "hikari::imagePreview.enlarge": "クリックで拡大",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "오류 복사",
46
46
  "hikari::errorBoundary.retry": "재시도",
47
47
  "hikari::errorBoundary.title": "컴포넌트 오류",
48
+ "hikari::filePicker.browse": "찾아보기…",
48
49
  "hikari::filePicker.cancel": "취소",
50
+ "hikari::filePicker.chooseFolder": "이 폴더 선택",
49
51
  "hikari::filePicker.confirm": "선택",
50
52
  "hikari::filePicker.copy": "복사",
51
53
  "hikari::filePicker.copyPath": "경로 복사",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "여러 원격 파일 선택…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "원격 파일 선택…",
58
60
  "hikari::filePicker.fieldClear": "선택 지우기",
61
+ "hikari::filePicker.fieldPickDirectory": "폴더 선택…",
62
+ "hikari::filePicker.fieldPickFile": "파일 선택…",
59
63
  "hikari::filePicker.filterAll": "모든 파일",
60
64
  "hikari::filePicker.gridLayout": "그리드 보기",
61
65
  "hikari::filePicker.listError": "이 폴더를 나열할 수 없습니다",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "다시 시도",
76
80
  "hikari::filePicker.size": "크기",
77
81
  "hikari::filePicker.title": "파일 선택",
82
+ "hikari::filePicker.titleFolder": "폴더 찾아보기",
78
83
  "hikari::imageLightbox.close": "닫기",
79
84
  "hikari::imageLightbox.title": "이미지 뷰어",
80
85
  "hikari::imagePreview.enlarge": "클릭하여 확대",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "Copiar erro",
46
46
  "hikari::errorBoundary.retry": "Tentar novamente",
47
47
  "hikari::errorBoundary.title": "Erro no componente",
48
+ "hikari::filePicker.browse": "Procurar…",
48
49
  "hikari::filePicker.cancel": "Cancelar",
50
+ "hikari::filePicker.chooseFolder": "Escolher esta pasta",
49
51
  "hikari::filePicker.confirm": "Escolher",
50
52
  "hikari::filePicker.copy": "Copiar",
51
53
  "hikari::filePicker.copyPath": "Copiar caminho",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "Escolher arquivos remotos…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "Escolher um arquivo remoto…",
58
60
  "hikari::filePicker.fieldClear": "Limpar seleção",
61
+ "hikari::filePicker.fieldPickDirectory": "Escolher uma pasta…",
62
+ "hikari::filePicker.fieldPickFile": "Escolher um ficheiro…",
59
63
  "hikari::filePicker.filterAll": "Todos os arquivos",
60
64
  "hikari::filePicker.gridLayout": "Visualização em grade",
61
65
  "hikari::filePicker.listError": "Não foi possível listar esta pasta",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "Tentar novamente",
76
80
  "hikari::filePicker.size": "Tamanho",
77
81
  "hikari::filePicker.title": "Escolher arquivos",
82
+ "hikari::filePicker.titleFolder": "Procurar pastas",
78
83
  "hikari::imageLightbox.close": "Fechar",
79
84
  "hikari::imageLightbox.title": "Visualizador de imagens",
80
85
  "hikari::imagePreview.enlarge": "Ampliar",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "Копировать ошибку",
46
46
  "hikari::errorBoundary.retry": "Повторить",
47
47
  "hikari::errorBoundary.title": "Ошибка компонента",
48
+ "hikari::filePicker.browse": "Обзор…",
48
49
  "hikari::filePicker.cancel": "Отмена",
50
+ "hikari::filePicker.chooseFolder": "Выбрать эту папку",
49
51
  "hikari::filePicker.confirm": "Выбрать",
50
52
  "hikari::filePicker.copy": "Копировать",
51
53
  "hikari::filePicker.copyPath": "Копировать путь",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "Выбрать удалённые файлы…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "Выбрать удалённый файл…",
58
60
  "hikari::filePicker.fieldClear": "Очистить выбор",
61
+ "hikari::filePicker.fieldPickDirectory": "Выбрать папку…",
62
+ "hikari::filePicker.fieldPickFile": "Выбрать файл…",
59
63
  "hikari::filePicker.filterAll": "Все файлы",
60
64
  "hikari::filePicker.gridLayout": "Сетка",
61
65
  "hikari::filePicker.listError": "Не удалось получить содержимое папки",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "Повторить",
76
80
  "hikari::filePicker.size": "Размер",
77
81
  "hikari::filePicker.title": "Выбор файлов",
82
+ "hikari::filePicker.titleFolder": "Обзор папок",
78
83
  "hikari::imageLightbox.close": "Закрыть",
79
84
  "hikari::imageLightbox.title": "Просмотр изображения",
80
85
  "hikari::imagePreview.enlarge": "Увеличить",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "复制错误",
46
46
  "hikari::errorBoundary.retry": "重试",
47
47
  "hikari::errorBoundary.title": "组件错误",
48
+ "hikari::filePicker.browse": "浏览…",
48
49
  "hikari::filePicker.cancel": "取消",
50
+ "hikari::filePicker.chooseFolder": "选择此文件夹",
49
51
  "hikari::filePicker.confirm": "选择",
50
52
  "hikari::filePicker.copy": "复制",
51
53
  "hikari::filePicker.copyPath": "复制路径",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "选择多个远程文件…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "选择远程文件…",
58
60
  "hikari::filePicker.fieldClear": "清除所选",
61
+ "hikari::filePicker.fieldPickDirectory": "选择文件夹…",
62
+ "hikari::filePicker.fieldPickFile": "选择文件…",
59
63
  "hikari::filePicker.filterAll": "所有文件",
60
64
  "hikari::filePicker.gridLayout": "网格视图",
61
65
  "hikari::filePicker.listError": "无法列出此文件夹",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "重试",
76
80
  "hikari::filePicker.size": "大小",
77
81
  "hikari::filePicker.title": "选择文件",
82
+ "hikari::filePicker.titleFolder": "浏览文件夹",
78
83
  "hikari::imageLightbox.close": "关闭",
79
84
  "hikari::imageLightbox.title": "图片查看器",
80
85
  "hikari::imagePreview.enlarge": "点击放大",
@@ -45,7 +45,9 @@
45
45
  "hikari::errorBoundary.copyError": "複製錯誤",
46
46
  "hikari::errorBoundary.retry": "重試",
47
47
  "hikari::errorBoundary.title": "元件錯誤",
48
+ "hikari::filePicker.browse": "瀏覽…",
48
49
  "hikari::filePicker.cancel": "取消",
50
+ "hikari::filePicker.chooseFolder": "選擇此資料夾",
49
51
  "hikari::filePicker.confirm": "選擇",
50
52
  "hikari::filePicker.copy": "複製",
51
53
  "hikari::filePicker.copyPath": "複製路徑",
@@ -56,6 +58,8 @@
56
58
  "hikari::filePicker.fieldChooseRemoteMany": "選擇多個遠端檔案…",
57
59
  "hikari::filePicker.fieldChooseRemoteOne": "選擇遠端檔案…",
58
60
  "hikari::filePicker.fieldClear": "清除所選",
61
+ "hikari::filePicker.fieldPickDirectory": "選擇資料夾…",
62
+ "hikari::filePicker.fieldPickFile": "選擇檔案…",
59
63
  "hikari::filePicker.filterAll": "所有檔案",
60
64
  "hikari::filePicker.gridLayout": "網格檢視",
61
65
  "hikari::filePicker.listError": "無法列出此資料夾",
@@ -75,6 +79,7 @@
75
79
  "hikari::filePicker.retry": "重試",
76
80
  "hikari::filePicker.size": "大小",
77
81
  "hikari::filePicker.title": "選擇檔案",
82
+ "hikari::filePicker.titleFolder": "瀏覽資料夾",
78
83
  "hikari::imageLightbox.close": "關閉",
79
84
  "hikari::imageLightbox.title": "圖片檢視器",
80
85
  "hikari::imagePreview.enlarge": "點擊放大",
package/src/index.ts CHANGED
@@ -74,12 +74,15 @@ export { default as HTag } from "./components/HkTag";
74
74
  export { default as HTextarea } from "./components/HkTextarea";
75
75
  export { default as HFileField } from "./components/HkFileField";
76
76
  export { default as HFileBrowserDialog } from "./components/HkFileBrowserDialog";
77
+ export { default as HFilePickerField } from "./components/HkFilePickerField";
77
78
  export {
78
79
  type RemoteFileEntry,
79
80
  type RemoteDirListing,
80
81
  type RemoteFsAdapter,
81
82
  type FileQuickLink,
82
83
  type PickedFile,
84
+ type FilePickerBackend,
85
+ type FilePickerHook,
83
86
  acceptExtensions,
84
87
  } from "./components/filePicker";
85
88
  export { default as HToast } from "./components/HkToast";