@ketvietlab/design-system 0.1.25 → 0.1.29
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/README.md +29 -0
- package/dist/atlas/island-runtime.d.mts +2 -0
- package/dist/atlas/island-runtime.d.mts.map +1 -0
- package/dist/atlas/island-runtime.mjs +2 -0
- package/dist/atlas/island-runtime.mjs.map +1 -0
- package/dist/atlas/relation-select-hydration.d.ts +2 -0
- package/dist/atlas/relation-select-hydration.d.ts.map +1 -0
- package/dist/atlas/relation-select-hydration.js +40 -0
- package/dist/atlas/relation-select-hydration.js.map +1 -0
- package/dist/atlas-cli.d.ts.map +1 -1
- package/dist/atlas-cli.js +18 -3
- package/dist/atlas-cli.js.map +1 -1
- package/dist/catalogue/index.js +1 -1
- package/dist/catalogue/inventory-policy.json +49 -0
- package/dist/catalogue/inventory.generated.js +1 -1
- package/dist/catalogue/inventory.generated.js.map +1 -1
- package/dist/catalogue/inventory.generated.json +2613 -1092
- package/dist/catalogue/registry.d.ts.map +1 -1
- package/dist/catalogue/registry.js +18 -0
- package/dist/catalogue/registry.js.map +1 -1
- package/dist/catalogue/specimens.d.ts.map +1 -1
- package/dist/catalogue/specimens.js +130 -10
- package/dist/catalogue/specimens.js.map +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +10 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/foundations/tokens.css +34 -26
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/interactions/ket-table/demo.d.ts +4 -0
- package/dist/interactions/ket-table/demo.d.ts.map +1 -0
- package/dist/interactions/ket-table/demo.js +147 -0
- package/dist/interactions/ket-table/demo.js.map +1 -0
- package/dist/interactions/ket-table/index.d.ts +199 -0
- package/dist/interactions/ket-table/index.d.ts.map +1 -0
- package/dist/interactions/ket-table/index.js +376 -0
- package/dist/interactions/ket-table/index.js.map +1 -0
- package/dist/interactions/ket-table/styles.css +282 -0
- package/dist/interactions/lightbox/demo.d.ts +3 -0
- package/dist/interactions/lightbox/demo.d.ts.map +1 -0
- package/dist/interactions/lightbox/demo.js +21 -0
- package/dist/interactions/lightbox/demo.js.map +1 -0
- package/dist/interactions/lightbox/index.d.ts +58 -0
- package/dist/interactions/lightbox/index.d.ts.map +1 -0
- package/dist/interactions/lightbox/index.js +220 -0
- package/dist/interactions/lightbox/index.js.map +1 -0
- package/dist/interactions/lightbox/styles.css +153 -0
- package/dist/interactions/relation-select/demo.d.ts +3 -0
- package/dist/interactions/relation-select/demo.d.ts.map +1 -0
- package/dist/interactions/relation-select/demo.js +36 -0
- package/dist/interactions/relation-select/demo.js.map +1 -0
- package/dist/interactions/relation-select/index.d.ts +87 -0
- package/dist/interactions/relation-select/index.d.ts.map +1 -0
- package/dist/interactions/relation-select/index.js +307 -0
- package/dist/interactions/relation-select/index.js.map +1 -0
- package/dist/interactions/relation-select/styles.css +311 -0
- package/dist/interactions/reorder-list/index.d.ts +23 -0
- package/dist/interactions/reorder-list/index.d.ts.map +1 -0
- package/dist/interactions/reorder-list/index.js +15 -0
- package/dist/interactions/reorder-list/index.js.map +1 -0
- package/dist/interactions/reorder-list/runtime.d.ts +5 -0
- package/dist/interactions/reorder-list/runtime.d.ts.map +1 -0
- package/dist/interactions/reorder-list/runtime.js +85 -0
- package/dist/interactions/reorder-list/runtime.js.map +1 -0
- package/dist/interactions/reorder-list/styles.css +33 -0
- package/dist/interactions/search-filter/demo.d.ts +3 -0
- package/dist/interactions/search-filter/demo.d.ts.map +1 -0
- package/dist/interactions/search-filter/demo.js +74 -0
- package/dist/interactions/search-filter/demo.js.map +1 -0
- package/dist/interactions/search-filter/index.d.ts +153 -0
- package/dist/interactions/search-filter/index.d.ts.map +1 -0
- package/dist/interactions/search-filter/index.js +824 -0
- package/dist/interactions/search-filter/index.js.map +1 -0
- package/dist/interactions/search-filter/styles.css +580 -0
- package/dist/layouts/app-navigation/index.d.ts +11 -5
- package/dist/layouts/app-navigation/index.d.ts.map +1 -1
- package/dist/layouts/app-navigation/index.js +5 -3
- package/dist/layouts/app-navigation/index.js.map +1 -1
- package/dist/layouts/app-navigation/styles.css +50 -0
- package/dist/layouts/page-identity/index.d.ts +2 -0
- package/dist/layouts/page-identity/index.d.ts.map +1 -1
- package/dist/layouts/page-identity/index.js +2 -2
- package/dist/layouts/page-identity/index.js.map +1 -1
- package/dist/layouts/shell/index.d.ts +34 -1
- package/dist/layouts/shell/index.d.ts.map +1 -1
- package/dist/layouts/shell/index.js +26 -2
- package/dist/layouts/shell/index.js.map +1 -1
- package/dist/layouts/shell/styles.css +283 -0
- package/dist/patterns/data-table/styles.css +1 -1
- package/dist/patterns/list-page/index.d.ts +10 -3
- package/dist/patterns/list-page/index.d.ts.map +1 -1
- package/dist/patterns/list-page/index.js +10 -6
- package/dist/patterns/list-page/index.js.map +1 -1
- package/dist/patterns/list-page/responsive.css +1 -5
- package/dist/patterns/list-page/styles.css +25 -3
- package/dist/patterns/modal-sheet/index.d.ts +2 -0
- package/dist/patterns/modal-sheet/index.d.ts.map +1 -1
- package/dist/patterns/modal-sheet/index.js +3 -1
- package/dist/patterns/modal-sheet/index.js.map +1 -1
- package/dist/patterns/record-form/styles.css +34 -0
- package/dist/primitives/actions/index.d.ts +4 -0
- package/dist/primitives/actions/index.d.ts.map +1 -1
- package/dist/primitives/actions/index.js +2 -2
- package/dist/primitives/actions/index.js.map +1 -1
- package/dist/primitives/actions/styles.css +9 -0
- package/dist/primitives/feedback/styles.css +4 -1
- package/dist/primitives/status/index.d.ts +12 -1
- package/dist/primitives/status/index.d.ts.map +1 -1
- package/dist/primitives/status/index.js +16 -1
- package/dist/primitives/status/index.js.map +1 -1
- package/dist/primitives/status/styles.css +28 -0
- package/dist/record/formatted-values/index.d.ts +2 -1
- package/dist/record/formatted-values/index.d.ts.map +1 -1
- package/dist/record/formatted-values/index.js +2 -1
- package/dist/record/formatted-values/index.js.map +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +129 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
@layer ket.app {
|
|
2
|
+
/* A Fancybox-style viewer: thumbnails open a full-screen stage above every
|
|
3
|
+
other layer, including a record modal, with zoom, pan and previous/next. */
|
|
4
|
+
:where([data-kv-design-system]) [data-ui="lightbox"] {
|
|
5
|
+
display: contents;
|
|
6
|
+
}
|
|
7
|
+
:where([data-kv-design-system]) [data-ui="lightbox-thumbs"] {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: var(--kv-space-2);
|
|
11
|
+
}
|
|
12
|
+
:where([data-kv-design-system]) [data-ui="lightbox-thumb"],
|
|
13
|
+
:where([data-kv-design-system]) [data-ui="lightbox-empty"] {
|
|
14
|
+
--lightbox-thumb-size: 5.5rem;
|
|
15
|
+
display: inline-grid;
|
|
16
|
+
place-items: center;
|
|
17
|
+
inline-size: var(--lightbox-thumb-size);
|
|
18
|
+
block-size: var(--lightbox-thumb-size);
|
|
19
|
+
flex: none;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
padding: 0;
|
|
22
|
+
border: 1px solid var(--kv-border-default);
|
|
23
|
+
border-radius: var(--kv-radius-md);
|
|
24
|
+
background: var(--kv-surface-raised);
|
|
25
|
+
color: var(--kv-text-muted);
|
|
26
|
+
font-size: var(--kv-text-sm);
|
|
27
|
+
text-align: center;
|
|
28
|
+
}
|
|
29
|
+
:where([data-kv-design-system]) [data-ui="lightbox-thumb"] {
|
|
30
|
+
cursor: zoom-in;
|
|
31
|
+
}
|
|
32
|
+
:where([data-kv-design-system]) [data-ui="lightbox-thumb"]:focus-visible {
|
|
33
|
+
outline: 2px solid var(--kv-focus-border);
|
|
34
|
+
outline-offset: 2px;
|
|
35
|
+
}
|
|
36
|
+
:where([data-kv-design-system]) [data-ui="lightbox-thumb"] img {
|
|
37
|
+
inline-size: 100%;
|
|
38
|
+
block-size: 100%;
|
|
39
|
+
object-fit: cover;
|
|
40
|
+
}
|
|
41
|
+
:where([data-kv-design-system])
|
|
42
|
+
:is([data-ui="lightbox-thumb"], [data-ui="lightbox-empty"])[data-size="small"] {
|
|
43
|
+
--lightbox-thumb-size: 2.5rem;
|
|
44
|
+
border-radius: var(--kv-radius-sm);
|
|
45
|
+
}
|
|
46
|
+
:where([data-kv-design-system])
|
|
47
|
+
:is([data-ui="lightbox-thumb"], [data-ui="lightbox-empty"])[data-size="large"] {
|
|
48
|
+
--lightbox-thumb-size: 9rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:where([data-kv-design-system]) [data-ui="lightbox-layer"] {
|
|
52
|
+
position: fixed;
|
|
53
|
+
inset: 0;
|
|
54
|
+
z-index: calc(var(--kv-z-modal) + 10);
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
57
|
+
color: var(--kv-panel-bg);
|
|
58
|
+
}
|
|
59
|
+
:where([data-kv-design-system]) [data-ui="lightbox-backdrop"] {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
background: color-mix(in srgb, var(--kv-text-main) 92%, transparent);
|
|
63
|
+
}
|
|
64
|
+
:where([data-kv-design-system]) [data-ui="lightbox-toolbar"] {
|
|
65
|
+
position: relative;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: flex-end;
|
|
69
|
+
gap: var(--kv-space-1);
|
|
70
|
+
padding: var(--kv-space-2) var(--kv-space-3);
|
|
71
|
+
}
|
|
72
|
+
:where([data-kv-design-system]) [data-ui="lightbox-counter"] {
|
|
73
|
+
margin-inline-end: auto;
|
|
74
|
+
font-size: var(--kv-text-sm);
|
|
75
|
+
font-variant-numeric: tabular-nums;
|
|
76
|
+
}
|
|
77
|
+
:where([data-kv-design-system]) :is([data-ui="lightbox-tool"], [data-ui="lightbox-nav"]) {
|
|
78
|
+
display: inline-grid;
|
|
79
|
+
place-items: center;
|
|
80
|
+
inline-size: 2.5rem;
|
|
81
|
+
block-size: 2.5rem;
|
|
82
|
+
padding: 0;
|
|
83
|
+
border: 0;
|
|
84
|
+
border-radius: 999px;
|
|
85
|
+
background: color-mix(in srgb, var(--kv-panel-bg) 12%, transparent);
|
|
86
|
+
color: inherit;
|
|
87
|
+
font-size: 1.5rem;
|
|
88
|
+
line-height: 1;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
:where([data-kv-design-system]) :is([data-ui="lightbox-tool"], [data-ui="lightbox-nav"]):hover {
|
|
92
|
+
background: color-mix(in srgb, var(--kv-panel-bg) 24%, transparent);
|
|
93
|
+
}
|
|
94
|
+
:where([data-kv-design-system]) :is([data-ui="lightbox-tool"], [data-ui="lightbox-nav"]):focus-visible {
|
|
95
|
+
outline: 2px solid var(--kv-panel-bg);
|
|
96
|
+
outline-offset: 2px;
|
|
97
|
+
}
|
|
98
|
+
:where([data-kv-design-system]) [data-ui="lightbox-tool"]:disabled {
|
|
99
|
+
opacity: 0.4;
|
|
100
|
+
cursor: default;
|
|
101
|
+
}
|
|
102
|
+
:where([data-kv-design-system]) [data-ui="lightbox-stage"] {
|
|
103
|
+
position: relative;
|
|
104
|
+
display: grid;
|
|
105
|
+
place-items: center;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
padding: var(--kv-space-2) var(--kv-space-12);
|
|
108
|
+
touch-action: none;
|
|
109
|
+
}
|
|
110
|
+
:where([data-kv-design-system]) [data-ui="lightbox-image"] {
|
|
111
|
+
max-inline-size: 100%;
|
|
112
|
+
max-block-size: 100%;
|
|
113
|
+
object-fit: contain;
|
|
114
|
+
transform-origin: center;
|
|
115
|
+
transition: transform 160ms ease-out;
|
|
116
|
+
cursor: zoom-in;
|
|
117
|
+
user-select: none;
|
|
118
|
+
}
|
|
119
|
+
:where([data-kv-design-system]) [data-ui="lightbox-stage"][data-zoomed="true"] [data-ui="lightbox-image"] {
|
|
120
|
+
cursor: grab;
|
|
121
|
+
transition: none;
|
|
122
|
+
}
|
|
123
|
+
:where([data-kv-design-system]) [data-ui="lightbox-nav"] {
|
|
124
|
+
position: absolute;
|
|
125
|
+
inset-block-start: 50%;
|
|
126
|
+
translate: 0 -50%;
|
|
127
|
+
}
|
|
128
|
+
:where([data-kv-design-system]) [data-ui="lightbox-nav"][data-direction="previous"] {
|
|
129
|
+
inset-inline-start: var(--kv-space-3);
|
|
130
|
+
}
|
|
131
|
+
:where([data-kv-design-system]) [data-ui="lightbox-nav"][data-direction="next"] {
|
|
132
|
+
inset-inline-end: var(--kv-space-3);
|
|
133
|
+
}
|
|
134
|
+
:where([data-kv-design-system]) [data-ui="lightbox-caption"] {
|
|
135
|
+
position: relative;
|
|
136
|
+
margin: 0;
|
|
137
|
+
padding: var(--kv-space-2) var(--kv-space-4) var(--kv-space-4);
|
|
138
|
+
font-size: var(--kv-text-sm);
|
|
139
|
+
text-align: center;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (max-width: 47.9375rem) {
|
|
143
|
+
:where([data-kv-design-system]) [data-ui="lightbox-stage"] {
|
|
144
|
+
padding-inline: var(--kv-space-2);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (prefers-reduced-motion: reduce) {
|
|
149
|
+
:where([data-kv-design-system]) [data-ui="lightbox-image"] {
|
|
150
|
+
transition: none;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../../../packages/design-system/src/interactions/relation-select/demo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAOvD,eAAO,MAAM,wBAAwB,EAAE,oBA6BtC,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared between the design-system's own catalogue specimen and the KetAtlas
|
|
2
|
+
// materializer, so both static demos show exactly the same interaction: a closed
|
|
3
|
+
// island with a real, multi-entry option list a reader can open, search and choose
|
|
4
|
+
// from. There is no `manager`, so search beyond these options, create and remove
|
|
5
|
+
// stay no-ops — neither demo has a backend behind it to call.
|
|
6
|
+
export const relationSelectDemoConfig = {
|
|
7
|
+
name: 'partnerId',
|
|
8
|
+
ariaLabel: 'Partner',
|
|
9
|
+
value: 'an-viet',
|
|
10
|
+
options: [
|
|
11
|
+
{ value: 'an-viet', label: 'An Việt', description: 'Hà Nội' },
|
|
12
|
+
{ value: 'song-xanh', label: 'Sông Xanh', description: 'Đà Nẵng' },
|
|
13
|
+
{ value: 'coop-mart', label: 'Coopmart', description: 'TP. Hồ Chí Minh' },
|
|
14
|
+
],
|
|
15
|
+
labels: {
|
|
16
|
+
choose: 'Choose a partner',
|
|
17
|
+
search: 'Search partners',
|
|
18
|
+
more: 'Browse all',
|
|
19
|
+
noRecords: 'No partners found',
|
|
20
|
+
loading: 'Loading…',
|
|
21
|
+
loadError: 'Could not load partners',
|
|
22
|
+
dialogTitle: 'Choose a partner',
|
|
23
|
+
close: 'Close',
|
|
24
|
+
select: 'Select',
|
|
25
|
+
create: 'Create partner',
|
|
26
|
+
edit: 'Edit',
|
|
27
|
+
save: 'Save',
|
|
28
|
+
cancel: 'Cancel',
|
|
29
|
+
remove: 'Remove',
|
|
30
|
+
confirmRemove: 'Confirm remove',
|
|
31
|
+
retry: 'Retry',
|
|
32
|
+
clear: 'Clear',
|
|
33
|
+
chosen: 'Chosen',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=demo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.js","sourceRoot":"","sources":["../../../../../../../packages/design-system/src/interactions/relation-select/demo.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,iFAAiF;AACjF,mFAAmF;AACnF,iFAAiF;AACjF,8DAA8D;AAC9D,MAAM,CAAC,MAAM,wBAAwB,GAAyB;IAC5D,IAAI,EAAE,WAAW;IACjB,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE;QACP,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE;QAC7D,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE;QAClE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE;KAC1E;IACD,MAAM,EAAE;QACN,MAAM,EAAE,kBAAkB;QAC1B,MAAM,EAAE,iBAAiB;QACzB,IAAI,EAAE,YAAY;QAClB,SAAS,EAAE,mBAAmB;QAC9B,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,yBAAyB;QACpC,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,OAAO;QACd,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,gBAAgB;QACxB,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,QAAQ;QAChB,aAAa,EAAE,gBAAgB;QAC/B,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,OAAO;QACd,MAAM,EAAE,QAAQ;KACjB;CACF,CAAA"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { IslandController, IslandProps } from '@ketvietlab/ketjs-view';
|
|
2
|
+
export declare const HOOKS: readonly ['relation-select', 'relation-native', 'relation-trigger', 'relation-value', 'relation-caret', 'relation-chips', 'relation-chip', 'relation-chip-clear', 'relation-menu', 'relation-search', 'relation-options', 'relation-option', 'relation-footer', 'relation-empty', 'relation-editor', 'relation-dialog-toolbar', 'relation-search-field', 'relation-search-icon', 'relation-dialog-list', 'relation-dialog-row', 'relation-dialog-main', 'relation-dialog-meta', 'relation-dialog-actions'];
|
|
3
|
+
export type RelationOption = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type RelationEditorField = {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
type?: 'text' | 'email' | 'tel' | 'select';
|
|
12
|
+
required?: boolean;
|
|
13
|
+
options?: RelationOption[];
|
|
14
|
+
};
|
|
15
|
+
export type RelationManager = {
|
|
16
|
+
listFunction: string;
|
|
17
|
+
listInput?: Record<string, unknown>;
|
|
18
|
+
searchParam?: string;
|
|
19
|
+
limitParam?: string;
|
|
20
|
+
limit?: number;
|
|
21
|
+
idField?: string;
|
|
22
|
+
labelField?: string;
|
|
23
|
+
descriptionField?: string;
|
|
24
|
+
saveFunction?: string;
|
|
25
|
+
saveDefaults?: Record<string, unknown>;
|
|
26
|
+
removeFunction?: string;
|
|
27
|
+
removeDefaults?: Record<string, unknown>;
|
|
28
|
+
fields?: RelationEditorField[];
|
|
29
|
+
excludeIds?: string[];
|
|
30
|
+
};
|
|
31
|
+
export type RelationSelectLabels = {
|
|
32
|
+
choose: string;
|
|
33
|
+
search: string;
|
|
34
|
+
more: string;
|
|
35
|
+
noRecords: string;
|
|
36
|
+
loading: string;
|
|
37
|
+
loadError: string;
|
|
38
|
+
dialogTitle: string;
|
|
39
|
+
close: string;
|
|
40
|
+
select: string;
|
|
41
|
+
create: string;
|
|
42
|
+
edit: string;
|
|
43
|
+
save: string;
|
|
44
|
+
cancel: string;
|
|
45
|
+
remove: string;
|
|
46
|
+
confirmRemove: string;
|
|
47
|
+
retry: string;
|
|
48
|
+
clear: string;
|
|
49
|
+
chosen: string;
|
|
50
|
+
};
|
|
51
|
+
export type RelationSelectConfig = {
|
|
52
|
+
name: string;
|
|
53
|
+
ariaLabel: string;
|
|
54
|
+
value?: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Several records under one field name, joined by a comma on the way out.
|
|
57
|
+
*
|
|
58
|
+
* A repeated form key would not survive `readForm`, which builds a
|
|
59
|
+
* `Record<string, string>` and keeps only the last value — so a multi-valued
|
|
60
|
+
* field posts one comma-separated string, which is also what the routes that
|
|
61
|
+
* take value lists already parse.
|
|
62
|
+
*/
|
|
63
|
+
multiple?: boolean;
|
|
64
|
+
values?: string[];
|
|
65
|
+
options: RelationOption[];
|
|
66
|
+
required?: boolean;
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
labels: RelationSelectLabels;
|
|
69
|
+
manager?: RelationManager;
|
|
70
|
+
};
|
|
71
|
+
/** Client-only composition hooks; keep callbacks out of serialized island props. */
|
|
72
|
+
export type RelationSelectCallbacks = {
|
|
73
|
+
options?: () => RelationOption[];
|
|
74
|
+
disabled?: () => boolean;
|
|
75
|
+
onSelect?: (option: RelationOption) => void;
|
|
76
|
+
onCreate?: (query: string) => void;
|
|
77
|
+
/** An add-to-list picker returns to its placeholder after each selection. */
|
|
78
|
+
resetAfterSelect?: boolean;
|
|
79
|
+
};
|
|
80
|
+
type RelationSelectIslandProps = IslandProps & {
|
|
81
|
+
id: string;
|
|
82
|
+
config: RelationSelectConfig;
|
|
83
|
+
};
|
|
84
|
+
export declare function createRelationSelectView(props: RelationSelectIslandProps, callbacks?: RelationSelectCallbacks): IslandController;
|
|
85
|
+
export declare const relationSelect: (props: IslandProps) => IslandController;
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../packages/design-system/src/interactions/relation-select/index.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@ketvietlab/ketjs-view/jsx-runtime";
|
|
2
|
+
// Unlike its neighbors here, this is a ketjs-view *island*: it owns reactive state
|
|
3
|
+
// (`signal`) and fetches its own data (`/_ket/fn/...`, a ketjs runtime contract, not
|
|
4
|
+
// an application-specific one) rather than being a static, server-rendered control
|
|
5
|
+
// wired up by the shared `attachDesignSystemInteractions` delegator. A consuming app
|
|
6
|
+
// mounts it through its own island runtime (see `defineIsland` in `@ketvietlab/ketjs-view`)
|
|
7
|
+
// and supplies the `manager` config that names which functions to call.
|
|
8
|
+
import { each, effect, signal } from '@ketvietlab/ketjs-view';
|
|
9
|
+
export const HOOKS = [
|
|
10
|
+
'relation-select',
|
|
11
|
+
'relation-native',
|
|
12
|
+
'relation-trigger',
|
|
13
|
+
'relation-value',
|
|
14
|
+
'relation-caret',
|
|
15
|
+
'relation-chips',
|
|
16
|
+
'relation-chip',
|
|
17
|
+
'relation-chip-clear',
|
|
18
|
+
'relation-menu',
|
|
19
|
+
'relation-search',
|
|
20
|
+
'relation-options',
|
|
21
|
+
'relation-option',
|
|
22
|
+
'relation-footer',
|
|
23
|
+
'relation-empty',
|
|
24
|
+
'relation-editor',
|
|
25
|
+
'relation-dialog-toolbar',
|
|
26
|
+
'relation-search-field',
|
|
27
|
+
'relation-search-icon',
|
|
28
|
+
'relation-dialog-list',
|
|
29
|
+
'relation-dialog-row',
|
|
30
|
+
'relation-dialog-main',
|
|
31
|
+
'relation-dialog-meta',
|
|
32
|
+
'relation-dialog-actions',
|
|
33
|
+
];
|
|
34
|
+
const array = (value) => (Array.isArray(value) ? value : []);
|
|
35
|
+
const string = (value) => (value == null ? '' : String(value));
|
|
36
|
+
const callApi = async (name, input, requestSignal) => {
|
|
37
|
+
const response = await fetch(`/_ket/fn/${encodeURIComponent(name)}`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
credentials: 'same-origin',
|
|
40
|
+
headers: { 'content-type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(input),
|
|
42
|
+
signal: requestSignal,
|
|
43
|
+
});
|
|
44
|
+
const payload = (await response.json());
|
|
45
|
+
if (!response.ok || payload.ok === false) {
|
|
46
|
+
const domainError = array(payload.errors)[0];
|
|
47
|
+
throw new Error(string(domainError?.message ?? domainError?.code ?? payload.message ?? `HTTP ${response.status}`));
|
|
48
|
+
}
|
|
49
|
+
return payload.value;
|
|
50
|
+
};
|
|
51
|
+
export function createRelationSelectView(props, callbacks = {}) {
|
|
52
|
+
const { id: islandId, config } = props;
|
|
53
|
+
const labels = config.labels;
|
|
54
|
+
const manager = config.manager;
|
|
55
|
+
// One representation for both modes: an array of chosen ids. Single-select is
|
|
56
|
+
// the array of length 0 or 1, so downstream code only branches where the UI
|
|
57
|
+
// genuinely differs — the trigger and the chips.
|
|
58
|
+
const multiple = config.multiple === true;
|
|
59
|
+
const chosen = signal(multiple
|
|
60
|
+
? array(config.values).map(string).filter(Boolean)
|
|
61
|
+
: [string(config.value)].filter(Boolean));
|
|
62
|
+
const selected = () => chosen()[0] ?? '';
|
|
63
|
+
const isChosen = (value) => chosen().includes(string(value));
|
|
64
|
+
const options = signal(array(config.options));
|
|
65
|
+
const availableOptions = () => callbacks.options?.() ?? options();
|
|
66
|
+
const disabled = () => callbacks.disabled?.() ?? config.disabled === true;
|
|
67
|
+
const open = signal(false);
|
|
68
|
+
const dialog = signal(false);
|
|
69
|
+
const query = signal('');
|
|
70
|
+
const rows = signal([]);
|
|
71
|
+
const loading = signal(false);
|
|
72
|
+
const error = signal('');
|
|
73
|
+
const editor = signal(null);
|
|
74
|
+
const pendingRemove = signal('');
|
|
75
|
+
let activeRequest = null;
|
|
76
|
+
let searchTimer;
|
|
77
|
+
let disposed = false;
|
|
78
|
+
const rowId = (row) => string(row[manager?.idField ?? 'id']);
|
|
79
|
+
const rowLabel = (row) => string(row[manager?.labelField ?? 'name']) || labels.noRecords;
|
|
80
|
+
const rowDescription = (row) => string(row[manager?.descriptionField ?? 'ref']);
|
|
81
|
+
const selectedLabel = () => options().find((entry) => string(entry.value) === selected())?.label ?? labels.choose;
|
|
82
|
+
const filteredOptions = () => {
|
|
83
|
+
const needle = query().trim().toLocaleLowerCase();
|
|
84
|
+
const held = needle
|
|
85
|
+
? availableOptions().filter((entry) => `${string(entry.label)} ${string(entry.description)}`.toLocaleLowerCase().includes(needle))
|
|
86
|
+
: availableOptions();
|
|
87
|
+
return callbacks.options ? held : held.slice(0, 7);
|
|
88
|
+
};
|
|
89
|
+
const choose = (value, label, description = '') => {
|
|
90
|
+
if (disabled())
|
|
91
|
+
return;
|
|
92
|
+
const id = string(value);
|
|
93
|
+
if (!options().some((entry) => string(entry.value) === id))
|
|
94
|
+
options.set([...options(), { value: id, label: string(label), description: string(description) }]);
|
|
95
|
+
if (multiple) {
|
|
96
|
+
chosen.set(isChosen(id) ? chosen().filter((held) => held !== id) : [...chosen(), id]);
|
|
97
|
+
callbacks.onSelect?.({ value: id, label: string(label), description: string(description) });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
chosen.set(id ? [id] : []);
|
|
101
|
+
open.set(false);
|
|
102
|
+
dialog.set(false);
|
|
103
|
+
query.set('');
|
|
104
|
+
editor.set(null);
|
|
105
|
+
pendingRemove.set('');
|
|
106
|
+
if (callbacks.resetAfterSelect)
|
|
107
|
+
chosen.set([]);
|
|
108
|
+
callbacks.onSelect?.({ value: id, label: string(label), description: string(description) });
|
|
109
|
+
};
|
|
110
|
+
const unchoose = (value) => {
|
|
111
|
+
chosen.set(chosen().filter((held) => held !== string(value)));
|
|
112
|
+
};
|
|
113
|
+
const labelOf = (value) => options().find((entry) => string(entry.value) === string(value))?.label ?? string(value);
|
|
114
|
+
const loadRows = async () => {
|
|
115
|
+
if (!manager?.listFunction)
|
|
116
|
+
return;
|
|
117
|
+
activeRequest?.abort();
|
|
118
|
+
const request = new AbortController();
|
|
119
|
+
activeRequest = request;
|
|
120
|
+
loading.set(true);
|
|
121
|
+
error.set('');
|
|
122
|
+
try {
|
|
123
|
+
const input = { ...(manager.listInput ?? {}) };
|
|
124
|
+
input[manager.searchParam ?? 'search'] = query().trim();
|
|
125
|
+
input[manager.limitParam ?? 'limit'] = manager.limit ?? 80;
|
|
126
|
+
const value = await callApi(manager.listFunction, input, request.signal);
|
|
127
|
+
const excluded = new Set(array(manager.excludeIds).map(string));
|
|
128
|
+
rows.set(array(value).filter((row) => !excluded.has(rowId(row))));
|
|
129
|
+
}
|
|
130
|
+
catch (caught) {
|
|
131
|
+
if (disposed || (caught instanceof Error && caught.name === 'AbortError'))
|
|
132
|
+
return;
|
|
133
|
+
error.set(caught instanceof Error ? caught.message : labels.loadError);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if (activeRequest === request)
|
|
137
|
+
loading.set(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const openDialog = async () => {
|
|
141
|
+
open.set(false);
|
|
142
|
+
dialog.set(true);
|
|
143
|
+
editor.set(null);
|
|
144
|
+
pendingRemove.set('');
|
|
145
|
+
await loadRows();
|
|
146
|
+
};
|
|
147
|
+
const closeDialog = () => {
|
|
148
|
+
dialog.set(false);
|
|
149
|
+
editor.set(null);
|
|
150
|
+
pendingRemove.set('');
|
|
151
|
+
error.set('');
|
|
152
|
+
};
|
|
153
|
+
const searchManager = (event) => {
|
|
154
|
+
if (!(event.currentTarget instanceof HTMLInputElement))
|
|
155
|
+
return;
|
|
156
|
+
query.set(event.currentTarget.value);
|
|
157
|
+
clearTimeout(searchTimer);
|
|
158
|
+
searchTimer = setTimeout(loadRows, 180);
|
|
159
|
+
};
|
|
160
|
+
const save = async (event) => {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
if (!manager?.saveFunction || !(event.currentTarget instanceof HTMLFormElement))
|
|
163
|
+
return;
|
|
164
|
+
const editing = editor();
|
|
165
|
+
const id = editing?.id || crypto.randomUUID();
|
|
166
|
+
const payload = { ...(manager.saveDefaults ?? {}), id };
|
|
167
|
+
for (const [name, value] of new FormData(event.currentTarget).entries())
|
|
168
|
+
payload[name] = string(value);
|
|
169
|
+
loading.set(true);
|
|
170
|
+
error.set('');
|
|
171
|
+
try {
|
|
172
|
+
await callApi(manager.saveFunction, payload);
|
|
173
|
+
const label = string(payload[manager.labelField ?? 'name']);
|
|
174
|
+
if (!editing?.id) {
|
|
175
|
+
choose(id, label);
|
|
176
|
+
if (multiple) {
|
|
177
|
+
editor.set(null);
|
|
178
|
+
await loadRows();
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
options.set(options().map((entry) => string(entry.value) === id ? { ...entry, label: label || entry.label } : entry));
|
|
183
|
+
editor.set(null);
|
|
184
|
+
await loadRows();
|
|
185
|
+
}
|
|
186
|
+
catch (caught) {
|
|
187
|
+
error.set(caught instanceof Error ? caught.message : labels.loadError);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
loading.set(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const remove = async (row) => {
|
|
194
|
+
const id = rowId(row);
|
|
195
|
+
if (pendingRemove() !== id) {
|
|
196
|
+
pendingRemove.set(id);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!manager?.removeFunction)
|
|
200
|
+
return;
|
|
201
|
+
loading.set(true);
|
|
202
|
+
error.set('');
|
|
203
|
+
try {
|
|
204
|
+
await callApi(manager.removeFunction, { ...(manager.removeDefaults ?? {}), id });
|
|
205
|
+
unchoose(id);
|
|
206
|
+
options.set(options().filter((entry) => string(entry.value) !== id));
|
|
207
|
+
pendingRemove.set('');
|
|
208
|
+
await loadRows();
|
|
209
|
+
}
|
|
210
|
+
catch (caught) {
|
|
211
|
+
error.set(caught instanceof Error ? caught.message : labels.loadError);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
loading.set(false);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const editorField = (field) => {
|
|
218
|
+
const held = editor()?.row ?? manager?.saveDefaults ?? {};
|
|
219
|
+
const value = string(held[field.name]);
|
|
220
|
+
const fieldId = `${islandId}-relation-editor-${field.name}`;
|
|
221
|
+
return (_jsxs("label", { "data-ui": "form-field", "data-span": "full", htmlFor: fieldId, children: [_jsxs("span", { "data-ui": "form-label", children: [field.label, field.required ? (_jsxs("span", { "data-ui": "form-required", "aria-hidden": "true", children: [' ', "*"] })) : null] }), field.type === 'select' ? (_jsx("select", { "data-ui": "form-control", id: fieldId, name: field.name, required: field.required === true, children: each(array(field.options), (entry) => entry.value, (entry) => (_jsx("option", { value: entry.value, selected: string(entry.value) === value, children: entry.label }))) })) : (_jsx("input", { "data-ui": "form-control", id: fieldId, type: field.type ?? 'text', name: field.name, value: value, autocomplete: "off", required: field.required === true }))] }));
|
|
222
|
+
};
|
|
223
|
+
const editorForm = () => (_jsxs("form", { "data-ui": "relation-editor", onSubmit: save, children: [_jsx("div", { "data-ui": "form-grid", children: each(array(manager?.fields), (field) => field.name, editorField) }), _jsx("div", { "data-ui": "form-actions", children: _jsxs("div", { "data-ui": "action-group", children: [_jsx("button", { "data-ui": "action", "data-variant": "primary", type: "submit", disabled: loading(), children: labels.save }), _jsx("button", { "data-ui": "action", "data-variant": "tertiary", type: "button", onClick: () => editor.set(null), children: labels.cancel })] }) })] }));
|
|
224
|
+
const managerRow = (row) => {
|
|
225
|
+
const id = rowId(row);
|
|
226
|
+
const description = rowDescription(row);
|
|
227
|
+
return (_jsxs("li", { "data-ui": "relation-dialog-row", "data-selected": String(isChosen(id)), children: [_jsxs("button", { "data-ui": "relation-dialog-main", type: "button", "aria-pressed": multiple ? String(isChosen(id)) : null, onClick: () => choose(id, rowLabel(row), description), children: [_jsx("strong", { children: rowLabel(row) }), description ? _jsx("small", { "data-ui": "relation-dialog-meta", children: description }) : null] }), _jsxs("div", { "data-ui": "relation-dialog-actions", children: [_jsx("button", { "data-ui": "action", "data-size": "compact", "data-variant": isChosen(id) ? 'primary' : 'secondary', type: "button", onClick: () => choose(id, rowLabel(row), description), children: isChosen(id) ? labels.chosen : labels.select }), manager?.saveFunction ? (_jsx("button", { "data-ui": "action", "data-size": "compact", "data-variant": "tertiary", type: "button", onClick: () => editor.set({ id, row }), children: labels.edit })) : null, manager?.removeFunction ? (_jsx("button", { "data-ui": "action", "data-size": "compact", "data-variant": "destructive", type: "button", onClick: () => remove(row), children: pendingRemove() === id ? labels.confirmRemove : labels.remove })) : null] })] }));
|
|
228
|
+
};
|
|
229
|
+
const dialogView = () => (_jsxs("div", { "data-ui": "modal-layer", "data-presentation": "dialog", children: [_jsx("button", { "data-ui": "modal-backdrop", type: "button", "aria-label": labels.close, onClick: closeDialog, children: _jsx("span", { children: labels.close }) }), _jsxs("section", { "data-ui": "modal-sheet", role: "dialog", "aria-modal": "true", "aria-labelledby": `${islandId}-relation-title`, children: [_jsxs("header", { "data-ui": "modal-head", children: [_jsx("h2", { "data-ui": "modal-title", id: `${islandId}-relation-title`, children: labels.dialogTitle }), _jsx("button", { "data-ui": "modal-close", type: "button", "aria-label": labels.close, title: labels.close, onClick: closeDialog, children: "\u00D7" })] }), _jsxs("div", { "data-ui": "modal-body", children: [_jsxs("div", { "data-ui": "relation-dialog-toolbar", children: [_jsxs("div", { "data-ui": "relation-search-field", children: [_jsx("span", { "data-ui": "relation-search-icon", "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", focusable: "false", children: [_jsx("circle", { cx: "11", cy: "11", r: "8" }), _jsx("path", { d: "m21 21-4.3-4.3" })] }) }), _jsx("input", { "data-ui": "field-control", type: "search", value: query(), autocomplete: "off", placeholder: labels.search, "aria-label": labels.search, onInput: searchManager })] }), manager?.saveFunction ? (_jsx("button", { "data-ui": "action", "data-variant": "primary", type: "button", onClick: () => editor.set({ id: '', row: manager.saveDefaults ?? {} }), children: labels.create })) : null] }), error() ? (_jsxs("aside", { "data-ui": "notice", "data-tone": "danger", role: "alert", children: [_jsxs("div", { "data-ui": "notice-copy", children: [_jsx("p", { "data-ui": "notice-title", children: labels.loadError }), _jsx("p", { "data-ui": "notice-message", children: error() })] }), _jsx("button", { "data-ui": "action", "data-variant": "secondary", type: "button", onClick: loadRows, children: labels.retry })] })) : null, editor() ? editorForm() : null, loading() ? (_jsx("p", { "data-ui": "relation-empty", role: "status", children: labels.loading })) : null, !loading() && !editor() ? (rows().length ? (_jsx("ul", { "data-ui": "relation-dialog-list", children: each(rows(), rowId, managerRow) })) : (_jsx("p", { "data-ui": "relation-empty", children: labels.noRecords }))) : null] })] })] }));
|
|
230
|
+
const handleKeydown = (event) => {
|
|
231
|
+
if (!(event instanceof KeyboardEvent) || event.key !== 'Escape')
|
|
232
|
+
return;
|
|
233
|
+
if (!open() && !dialog())
|
|
234
|
+
return;
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
event.stopPropagation();
|
|
237
|
+
const trigger = event.currentTarget?.querySelector('[data-ui="relation-trigger"]');
|
|
238
|
+
open.set(false);
|
|
239
|
+
closeDialog();
|
|
240
|
+
trigger?.focus();
|
|
241
|
+
};
|
|
242
|
+
const handleInvalid = (event) => {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
open.set(true);
|
|
245
|
+
if (event.currentTarget instanceof HTMLSelectElement)
|
|
246
|
+
event.currentTarget.nextElementSibling?.focus();
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
view: () => (_jsxs("div", { "data-ui": "relation-select", role: "group", "aria-label": config.ariaLabel, onKeydown: handleKeydown, children: [_jsxs("select", { "data-ui": "relation-native", name: config.name, required: config.required === true, disabled: disabled(), tabindex: "-1", "aria-hidden": "true", onInvalid: handleInvalid, children: [_jsx("option", { value: "", selected: !chosen().length }), multiple ? (chosen().length ? (_jsx("option", { value: chosen().join(','), selected: true, children: chosen().map(labelOf).join(', ') })) : null) : (each(options().filter((entry) => string(entry.value)), (entry) => entry.value, (entry) => (_jsx("option", { value: entry.value, selected: string(entry.value) === selected(), children: entry.label }))))] }), _jsxs("button", { "data-ui": "relation-trigger", "data-empty": String(!chosen().length), type: "button", "aria-haspopup": "listbox", "aria-label": config.ariaLabel, "aria-expanded": String(open()), "aria-controls": `${islandId}-relation-menu`, disabled: disabled(), onClick: () => open.set(!open()), children: [_jsx("span", { "data-ui": "relation-value", children: multiple ? labels.choose : selectedLabel() }), _jsx("span", { "data-ui": "relation-caret", "aria-hidden": "true", children: "\u25BE" })] }), multiple && chosen().length ? (_jsx("ul", { "data-ui": "relation-chips", "aria-label": labels.chosen, children: each(chosen(), (value) => value, (value) => (_jsxs("li", { "data-ui": "relation-chip", children: [_jsx("span", { children: labelOf(value) }), _jsx("button", { "data-ui": "relation-chip-clear", type: "button", "aria-label": `${labels.clear}: ${labelOf(value)}`, title: labels.clear, disabled: disabled(), onClick: () => unchoose(value), children: "\u00D7" })] }))) })) : null, open() ? (_jsxs("div", { "data-ui": "relation-menu", id: `${islandId}-relation-menu`, children: [_jsx("input", { "data-ui": "relation-search", type: "search", value: query(), autocomplete: "off", placeholder: labels.search, "aria-label": labels.search, onInput: (event) => {
|
|
250
|
+
if (event.currentTarget instanceof HTMLInputElement)
|
|
251
|
+
query.set(event.currentTarget.value);
|
|
252
|
+
} }), _jsx("div", { "data-ui": "relation-options", role: "listbox", children: filteredOptions().length ? (each(filteredOptions(), (entry) => entry.value, (entry) => (_jsxs("button", { "data-ui": "relation-option", type: "button", role: "option", "data-selected": String(isChosen(entry.value)), "aria-selected": String(isChosen(entry.value)), onClick: () => choose(entry.value, entry.label, entry.description), children: [_jsx("span", { children: entry.label }), entry.description ? _jsx("small", { children: entry.description }) : null] })))) : (_jsx("p", { "data-ui": "relation-empty", children: labels.noRecords })) }), manager || !callbacks.options || callbacks.onCreate ? (_jsxs("footer", { "data-ui": "relation-footer", children: [manager || !callbacks.options ? (_jsx("button", { "data-ui": "action", "data-variant": "tertiary", type: "button", onClick: openDialog, children: labels.more })) : null, callbacks.onCreate ? (_jsx("button", { "data-ui": "action", "data-variant": "tertiary", type: "button", disabled: disabled(), onClick: () => {
|
|
253
|
+
if (disabled())
|
|
254
|
+
return;
|
|
255
|
+
const name = query().trim();
|
|
256
|
+
open.set(false);
|
|
257
|
+
query.set('');
|
|
258
|
+
callbacks.onCreate?.(name);
|
|
259
|
+
}, children: labels.create })) : null] })) : null] })) : null, dialog() ? dialogView() : null] })),
|
|
260
|
+
// The hidden native select stands in for a real one — including firing
|
|
261
|
+
// `change` for whatever the host page already listens for there (a state
|
|
262
|
+
// recomputed off another field's selection, say). Skip the effect's own
|
|
263
|
+
// eager first run: nothing has changed yet, only settled onto its initial value.
|
|
264
|
+
// The dispatch itself waits a microtask: firing it synchronously, still
|
|
265
|
+
// inside the signal write that is choosing this very value, let a listener
|
|
266
|
+
// that reacts by re-rendering (and so disposing and re-mounting this same
|
|
267
|
+
// island) re-enter this instance's own `choose()` while it was still on the
|
|
268
|
+
// stack — deferring runs it only once the choice has fully settled.
|
|
269
|
+
mount: ({ root, lifetime }) => {
|
|
270
|
+
let first = true;
|
|
271
|
+
const stop = effect(() => {
|
|
272
|
+
chosen();
|
|
273
|
+
if (first) {
|
|
274
|
+
first = false;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
queueMicrotask(() => {
|
|
278
|
+
if (disposed)
|
|
279
|
+
return;
|
|
280
|
+
const native = [...root.querySelectorAll('[data-ui="relation-native"]')][0];
|
|
281
|
+
if (native instanceof HTMLSelectElement)
|
|
282
|
+
native.dispatchEvent(new Event('change', { bubbles: true }));
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
lifetime.addEventListener('abort', stop);
|
|
286
|
+
// The inline dropdown isn't a <details> — nothing in the shared
|
|
287
|
+
// `attachDesignSystemInteractions` outside-click delegator sees it, so it
|
|
288
|
+
// closes itself the same way every other popup does. The dialog already
|
|
289
|
+
// closes on its own backdrop click.
|
|
290
|
+
document.addEventListener('click', (event) => {
|
|
291
|
+
if (!open())
|
|
292
|
+
return;
|
|
293
|
+
const target = event.target;
|
|
294
|
+
if (target instanceof Node && root.contains(target))
|
|
295
|
+
return;
|
|
296
|
+
open.set(false);
|
|
297
|
+
}, { signal: lifetime });
|
|
298
|
+
},
|
|
299
|
+
dispose: () => {
|
|
300
|
+
disposed = true;
|
|
301
|
+
clearTimeout(searchTimer);
|
|
302
|
+
activeRequest?.abort();
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
export const relationSelect = (props) => createRelationSelectView(props);
|
|
307
|
+
//# sourceMappingURL=index.js.map
|