@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 +1 -1
- package/src/DemoApp.tsx +29 -1
- package/src/components/HkCheckbox.tsx +0 -1
- package/src/components/HkEmptyState.test.tsx +21 -1
- package/src/components/HkEmptyState.tsx +1 -1
- package/src/components/HkFileBrowserDialog.test.tsx +43 -0
- package/src/components/HkFileBrowserDialog.tsx +28 -4
- package/src/components/HkFilePickerField.scss +93 -0
- package/src/components/HkFilePickerField.test.tsx +194 -0
- package/src/components/HkFilePickerField.tsx +212 -0
- package/src/components/HkRadio.tsx +1 -1
- package/src/components/HkSwitch.tsx +1 -1
- package/src/components/filePicker.ts +16 -0
- package/src/i18n/locales/ar/components.json +5 -0
- package/src/i18n/locales/de/components.json +5 -0
- package/src/i18n/locales/en/components.json +5 -0
- package/src/i18n/locales/es/components.json +5 -0
- package/src/i18n/locales/fr/components.json +5 -0
- package/src/i18n/locales/ja/components.json +5 -0
- package/src/i18n/locales/ko/components.json +5 -0
- package/src/i18n/locales/pt/components.json +5 -0
- package/src/i18n/locales/ru/components.json +5 -0
- package/src/i18n/locales/zh-Hans/components.json +5 -0
- package/src/i18n/locales/zh-Hant/components.json +5 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
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 =
|
|
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">
|
|
@@ -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 ??
|
|
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={
|
|
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
|
+
});
|
|
@@ -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";
|