@kahitsan/ksui 0.31.0 → 0.32.0
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/components/base/Tooltip.tsx +12 -1
- package/src/components/composite/AccountRadioPicker.tsx +120 -0
- package/src/components/composite/FormAdvancedSection.tsx +366 -0
- package/src/components/composite/SalesBodyEditor.tsx +418 -0
- package/src/components/composite/TransactionForm.tsx +1053 -0
- package/src/components/composite/TransferAccountsPicker.tsx +291 -0
- package/src/components/composite/TransferFeeChip.tsx +34 -0
- package/src/index.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/ksui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -5,6 +5,14 @@ interface TooltipProps {
|
|
|
5
5
|
content: JSX.Element;
|
|
6
6
|
/** Where the bubble sits relative to the trigger. Default "top". */
|
|
7
7
|
placement?: "top" | "bottom";
|
|
8
|
+
/** Horizontal anchor. "center" (default) centers the bubble on the trigger;
|
|
9
|
+
* "start" left-aligns it to the trigger's left edge instead — use when the
|
|
10
|
+
* trigger sits near a left edge (sidebar, viewport) where a centered bubble
|
|
11
|
+
* would slide underneath it. */
|
|
12
|
+
align?: "center" | "start";
|
|
13
|
+
/** Wrap long copy inside a fixed-width bubble instead of one nowrap line —
|
|
14
|
+
* pairs with `align="start"` for descriptive copy near a screen edge. */
|
|
15
|
+
wrap?: boolean;
|
|
8
16
|
/** Allow the trigger to be any element; the wrapper is a span. */
|
|
9
17
|
children: JSX.Element;
|
|
10
18
|
/** Extra classes on the wrapper. The wrapper is inline-flex so it sits
|
|
@@ -43,6 +51,7 @@ interface TooltipProps {
|
|
|
43
51
|
export default function Tooltip(props: TooltipProps): JSX.Element {
|
|
44
52
|
const tooltipId = props.id ?? createUniqueId();
|
|
45
53
|
const placement = () => props.placement ?? "top";
|
|
54
|
+
const align = () => props.align ?? "center";
|
|
46
55
|
return (
|
|
47
56
|
<span
|
|
48
57
|
class={`relative inline-flex group/tt ${props.class ?? ""}`}
|
|
@@ -53,7 +62,9 @@ export default function Tooltip(props: TooltipProps): JSX.Element {
|
|
|
53
62
|
<span
|
|
54
63
|
role="tooltip"
|
|
55
64
|
id={tooltipId}
|
|
56
|
-
class={`pointer-events-none absolute
|
|
65
|
+
class={`pointer-events-none absolute px-2 py-1 rounded-md bg-zinc-900 border border-zinc-700/80 text-[11px] text-zinc-100 opacity-0 group-hover/tt:opacity-100 group-focus-within/tt:opacity-100 transition-opacity duration-150 delay-150 z-50 shadow-lg ${
|
|
66
|
+
align() === "start" ? "left-0" : "left-1/2 -translate-x-1/2"
|
|
67
|
+
} ${props.wrap ? "whitespace-normal w-max max-w-[260px]" : "whitespace-nowrap"} ${
|
|
57
68
|
placement() === "bottom" ? "top-full mt-1" : "bottom-full mb-1"
|
|
58
69
|
}`}
|
|
59
70
|
>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createEffect, Show, For } from "solid-js";
|
|
2
|
+
import AccountAvatar from "../base/AccountAvatar";
|
|
3
|
+
import type { PaymentAccountOption } from "./PaymentAccountPicker";
|
|
4
|
+
|
|
5
|
+
export interface AccountRadioPickerProps {
|
|
6
|
+
accounts: PaymentAccountOption[];
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (v: string) => void;
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
excludeId?: string;
|
|
11
|
+
autoDefault?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function AccountRadioPicker(props: AccountRadioPickerProps) {
|
|
15
|
+
const visible = () =>
|
|
16
|
+
props.accounts.filter(
|
|
17
|
+
(a) => !props.excludeId || a.id.toString() !== props.excludeId
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
createEffect(() => {
|
|
21
|
+
if (props.autoDefault === false) return;
|
|
22
|
+
if (props.value) return;
|
|
23
|
+
const first = visible()[0];
|
|
24
|
+
if (first) props.onChange(first.id.toString());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const buttonRefs: (HTMLButtonElement | undefined)[] = [];
|
|
28
|
+
|
|
29
|
+
const currentIndex = () => {
|
|
30
|
+
const list = visible();
|
|
31
|
+
const i = list.findIndex((a) => a.id.toString() === props.value);
|
|
32
|
+
return i >= 0 ? i : 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const selectByIndex = (idx: number) => {
|
|
36
|
+
const list = visible();
|
|
37
|
+
if (list.length === 0) return;
|
|
38
|
+
const wrapped = ((idx % list.length) + list.length) % list.length;
|
|
39
|
+
props.onChange(list[wrapped].id.toString());
|
|
40
|
+
buttonRefs[wrapped]?.focus();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
44
|
+
switch (e.key) {
|
|
45
|
+
case "ArrowRight":
|
|
46
|
+
case "ArrowDown":
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
selectByIndex(currentIndex() + 1);
|
|
49
|
+
break;
|
|
50
|
+
case "ArrowLeft":
|
|
51
|
+
case "ArrowUp":
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
selectByIndex(currentIndex() - 1);
|
|
54
|
+
break;
|
|
55
|
+
case "Home":
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
selectByIndex(0);
|
|
58
|
+
break;
|
|
59
|
+
case "End":
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
selectByIndex(visible().length - 1);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Show
|
|
68
|
+
when={props.accounts.length > 0}
|
|
69
|
+
fallback={
|
|
70
|
+
<input
|
|
71
|
+
type="number"
|
|
72
|
+
value={props.value}
|
|
73
|
+
onInput={(e) => props.onChange(e.currentTarget.value)}
|
|
74
|
+
aria-label={props.ariaLabel}
|
|
75
|
+
placeholder="Account ID (financial-accounts module unavailable)"
|
|
76
|
+
class="w-full bg-zinc-900/60 border border-zinc-800/60 px-3 py-3 text-sm text-zinc-200 ks-hud-clip-button focus:outline-none focus:border-amber-500/50"
|
|
77
|
+
/>
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
role="radiogroup"
|
|
82
|
+
aria-label={props.ariaLabel}
|
|
83
|
+
tabIndex={-1}
|
|
84
|
+
class="grid max-sm:grid-cols-2 sm:grid-cols-3 gap-2"
|
|
85
|
+
onKeyDown={onKeyDown}
|
|
86
|
+
>
|
|
87
|
+
<For each={visible()}>
|
|
88
|
+
{(a, i) => {
|
|
89
|
+
const selected = () => props.value === a.id.toString();
|
|
90
|
+
const isTabStop = () => selected() || (!props.value && i() === 0);
|
|
91
|
+
return (
|
|
92
|
+
<button
|
|
93
|
+
ref={(el) => (buttonRefs[i()] = el)}
|
|
94
|
+
type="button"
|
|
95
|
+
role="radio"
|
|
96
|
+
aria-checked={selected()}
|
|
97
|
+
tabIndex={isTabStop() ? 0 : -1}
|
|
98
|
+
onClick={() => props.onChange(a.id.toString())}
|
|
99
|
+
class="group flex items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm transition-colors cursor-pointer ks-hud-clip-top-left-bottom-right"
|
|
100
|
+
classList={{
|
|
101
|
+
"border-amber-500/50 bg-amber-600/10 text-amber-300":
|
|
102
|
+
selected(),
|
|
103
|
+
"border-zinc-700 bg-zinc-800/50 text-zinc-300 hover:border-zinc-600 hover:bg-zinc-800":
|
|
104
|
+
!selected(),
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<AccountAvatar
|
|
108
|
+
account={a}
|
|
109
|
+
size={24}
|
|
110
|
+
iconClass={selected() ? "text-amber-300" : "text-zinc-400"}
|
|
111
|
+
/>
|
|
112
|
+
<span class="truncate">{a.name}</span>
|
|
113
|
+
</button>
|
|
114
|
+
);
|
|
115
|
+
}}
|
|
116
|
+
</For>
|
|
117
|
+
</div>
|
|
118
|
+
</Show>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// The advanced-fields container of the transaction create/edit form: the Tax
|
|
2
|
+
// type 4-way toggle + VAT breakdown preview, the EWT checkbox/rate/preview, and
|
|
3
|
+
// the full Private-transaction panel (toggle, share-with-role buttons, per-member
|
|
4
|
+
// checkbox list). Carved verbatim out of TransactionForm; the form still owns the
|
|
5
|
+
// `viewMode === "advanced"` <Show> gate, so toggle behavior is unchanged. Props
|
|
6
|
+
// are a verbatim subset of TransactionFormProps.
|
|
7
|
+
|
|
8
|
+
import { For, Show } from "solid-js";
|
|
9
|
+
import Lock from "lucide-solid/icons/lock";
|
|
10
|
+
import FormField from "../base/FormField";
|
|
11
|
+
import SegmentedFilter from "../base/SegmentedFilter";
|
|
12
|
+
import { formatPHP } from "../../utils/formatPHP";
|
|
13
|
+
import type { TransactionOrgMember, TransactionShareableRole } from "./TransactionForm";
|
|
14
|
+
|
|
15
|
+
export interface FormAdvancedSectionProps {
|
|
16
|
+
amount: string;
|
|
17
|
+
category: string;
|
|
18
|
+
taxType: string;
|
|
19
|
+
setTaxType: (v: string) => void;
|
|
20
|
+
hasEwt: boolean;
|
|
21
|
+
setHasEwt: (v: boolean) => void;
|
|
22
|
+
ewtRate: string;
|
|
23
|
+
setEwtRate: (v: string) => void;
|
|
24
|
+
isPrivate: boolean;
|
|
25
|
+
setIsPrivate: (v: boolean) => void;
|
|
26
|
+
sharedWith: string[];
|
|
27
|
+
setSharedWith: (v: string[]) => void;
|
|
28
|
+
sharedRoleCodes: string[];
|
|
29
|
+
setSharedRoleCodes: (v: string[]) => void;
|
|
30
|
+
orgMembers: TransactionOrgMember[];
|
|
31
|
+
shareableRoles: TransactionShareableRole[];
|
|
32
|
+
canShare: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-testid="advanced-fields-container"
|
|
39
|
+
class="rounded-lg border border-zinc-700/60 bg-zinc-900/40 p-3 space-y-3"
|
|
40
|
+
>
|
|
41
|
+
<div class="flex items-center gap-2 text-[10px] uppercase tracking-widest text-zinc-400 font-semibold">
|
|
42
|
+
<span>Advanced</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="rounded-lg border border-zinc-800/50 bg-zinc-950/40 p-3">
|
|
46
|
+
<FormField label="Tax">
|
|
47
|
+
<SegmentedFilter
|
|
48
|
+
options={[
|
|
49
|
+
{ value: "vat_inclusive", label: "VAT Incl." },
|
|
50
|
+
{ value: "vat_exclusive", label: "VAT Excl." },
|
|
51
|
+
{ value: "vat_exempt", label: "Exempt" },
|
|
52
|
+
{ value: "non_vat", label: "Non-VAT" },
|
|
53
|
+
]}
|
|
54
|
+
value={props.taxType}
|
|
55
|
+
onChange={props.setTaxType}
|
|
56
|
+
/>
|
|
57
|
+
</FormField>
|
|
58
|
+
<Show
|
|
59
|
+
when={
|
|
60
|
+
props.amount &&
|
|
61
|
+
parseFloat(props.amount) > 0 &&
|
|
62
|
+
(props.taxType === "vat_inclusive" ||
|
|
63
|
+
props.taxType === "vat_exclusive")
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
{(() => {
|
|
67
|
+
const amt = parseFloat(props.amount);
|
|
68
|
+
const sub =
|
|
69
|
+
props.taxType === "vat_inclusive"
|
|
70
|
+
? Math.round((amt / 1.12) * 100) / 100
|
|
71
|
+
: amt;
|
|
72
|
+
const vat =
|
|
73
|
+
props.taxType === "vat_inclusive"
|
|
74
|
+
? Math.round((amt - sub) * 100) / 100
|
|
75
|
+
: Math.round(amt * 0.12 * 100) / 100;
|
|
76
|
+
const total = props.taxType === "vat_exclusive" ? sub + vat : amt;
|
|
77
|
+
return (
|
|
78
|
+
<div class="mt-2 text-xs text-zinc-500 space-y-0.5 border-t border-zinc-800/50 pt-2">
|
|
79
|
+
<div class="flex justify-between">
|
|
80
|
+
<span>VATtable Sales</span>
|
|
81
|
+
<span>{formatPHP(sub)}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="flex justify-between">
|
|
84
|
+
<span>VAT (12%)</span>
|
|
85
|
+
<span>{formatPHP(vat)}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex justify-between font-medium text-zinc-300">
|
|
88
|
+
<span>Total</span>
|
|
89
|
+
<span>{formatPHP(total)}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
})()}
|
|
94
|
+
</Show>
|
|
95
|
+
|
|
96
|
+
<Show
|
|
97
|
+
when={props.category === "expense" || props.category === "payable"}
|
|
98
|
+
>
|
|
99
|
+
<div class="mt-3 border-t border-zinc-800/50 pt-3">
|
|
100
|
+
<label class="flex items-center gap-2 text-xs text-zinc-300 cursor-pointer select-none">
|
|
101
|
+
<input
|
|
102
|
+
type="checkbox"
|
|
103
|
+
checked={props.hasEwt}
|
|
104
|
+
onChange={(e) => props.setHasEwt(e.currentTarget.checked)}
|
|
105
|
+
class="h-4 w-4 accent-amber-500 cursor-pointer"
|
|
106
|
+
/>
|
|
107
|
+
<span>Has Expanded Withholding Tax</span>
|
|
108
|
+
</label>
|
|
109
|
+
<Show when={props.hasEwt}>
|
|
110
|
+
<div class="mt-2 space-y-2">
|
|
111
|
+
<FormField label="EWT rate (%)">
|
|
112
|
+
<input
|
|
113
|
+
type="number"
|
|
114
|
+
step="0.01"
|
|
115
|
+
min="0.01"
|
|
116
|
+
max="100"
|
|
117
|
+
value={props.ewtRate}
|
|
118
|
+
onInput={(e) => props.setEwtRate(e.currentTarget.value)}
|
|
119
|
+
class="w-full px-3 py-2 bg-zinc-900/50 border border-zinc-800 text-zinc-100 text-sm rounded-md focus:outline-none focus:border-amber-500/50"
|
|
120
|
+
placeholder="e.g. 1, 2, 5, 10, 15"
|
|
121
|
+
/>
|
|
122
|
+
</FormField>
|
|
123
|
+
<Show
|
|
124
|
+
when={
|
|
125
|
+
props.amount &&
|
|
126
|
+
parseFloat(props.amount) > 0 &&
|
|
127
|
+
props.ewtRate &&
|
|
128
|
+
parseFloat(props.ewtRate) > 0
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
{(() => {
|
|
132
|
+
const amt = parseFloat(props.amount);
|
|
133
|
+
const rate = parseFloat(props.ewtRate);
|
|
134
|
+
const ewt = Math.round(amt * rate) / 100;
|
|
135
|
+
return (
|
|
136
|
+
<div class="text-xs text-zinc-500 border-t border-zinc-800/50 pt-2">
|
|
137
|
+
<div class="flex justify-between">
|
|
138
|
+
<span>EWT ({rate}%)</span>
|
|
139
|
+
<span>{formatPHP(ewt)}</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
})()}
|
|
144
|
+
</Show>
|
|
145
|
+
</div>
|
|
146
|
+
</Show>
|
|
147
|
+
</div>
|
|
148
|
+
</Show>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div
|
|
152
|
+
class="rounded-lg border border-zinc-800/50 bg-zinc-950/40 p-4 relative"
|
|
153
|
+
classList={{ "opacity-60": !props.canShare }}
|
|
154
|
+
title={
|
|
155
|
+
props.canShare
|
|
156
|
+
? undefined
|
|
157
|
+
: "Sharing private transactions requires the members.list_basic permission."
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
<div class="flex items-center justify-between min-h-[44px]">
|
|
161
|
+
<div class="flex items-center gap-2">
|
|
162
|
+
<Lock size={14} class="text-zinc-500" />
|
|
163
|
+
<div>
|
|
164
|
+
<span class="text-sm text-zinc-300">Private transaction</span>
|
|
165
|
+
<p class="text-[10px] text-zinc-600">
|
|
166
|
+
{props.canShare
|
|
167
|
+
? "Hidden from others unless shared"
|
|
168
|
+
: "Locked — needs members.list_basic permission"}
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
disabled={!props.canShare}
|
|
175
|
+
aria-label="Toggle private transaction"
|
|
176
|
+
onClick={() => {
|
|
177
|
+
if (!props.canShare) return;
|
|
178
|
+
props.setIsPrivate(!props.isPrivate);
|
|
179
|
+
}}
|
|
180
|
+
class="ks-theme-toggle shrink-0"
|
|
181
|
+
classList={{ "cursor-not-allowed": !props.canShare }}
|
|
182
|
+
>
|
|
183
|
+
<span
|
|
184
|
+
class="ks-theme-toggle-track"
|
|
185
|
+
data-active={props.isPrivate ? "" : undefined}
|
|
186
|
+
>
|
|
187
|
+
<span class="ks-theme-toggle-icon ks-theme-toggle-icon-moon">
|
|
188
|
+
<Lock size={12} />
|
|
189
|
+
</span>
|
|
190
|
+
<span class="ks-theme-toggle-icon ks-theme-toggle-icon-sun">
|
|
191
|
+
<Lock size={12} />
|
|
192
|
+
</span>
|
|
193
|
+
</span>
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<Show when={props.isPrivate && props.canShare}>
|
|
198
|
+
<div class="mt-4 pt-3 border-t border-zinc-800/50">
|
|
199
|
+
<p class="text-[10px] text-zinc-500 mb-3">
|
|
200
|
+
Always visible to:{" "}
|
|
201
|
+
<span class="text-zinc-400">
|
|
202
|
+
you (creator), org admins, superusers
|
|
203
|
+
</span>
|
|
204
|
+
</p>
|
|
205
|
+
|
|
206
|
+
<span class="text-xs text-zinc-500 block mb-2">
|
|
207
|
+
Share with role
|
|
208
|
+
</span>
|
|
209
|
+
<div class="flex gap-2 mb-3 flex-wrap">
|
|
210
|
+
<For each={props.shareableRoles}>
|
|
211
|
+
{(role) => {
|
|
212
|
+
const members = () =>
|
|
213
|
+
Array.isArray(props.orgMembers) ? props.orgMembers : [];
|
|
214
|
+
const membersInRole = () =>
|
|
215
|
+
members().filter((m) => m.role === role.code);
|
|
216
|
+
const selected = () =>
|
|
217
|
+
props.sharedRoleCodes.includes(role.code);
|
|
218
|
+
return (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={() => {
|
|
222
|
+
if (selected()) {
|
|
223
|
+
props.setSharedRoleCodes(
|
|
224
|
+
props.sharedRoleCodes.filter((c) => c !== role.code)
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
const roleMemberIds = membersInRole().map(
|
|
228
|
+
(m) => m.user_id
|
|
229
|
+
);
|
|
230
|
+
props.setSharedWith(
|
|
231
|
+
props.sharedWith.filter(
|
|
232
|
+
(id) => !roleMemberIds.includes(id)
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
props.setSharedRoleCodes([
|
|
236
|
+
...props.sharedRoleCodes,
|
|
237
|
+
role.code,
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
}}
|
|
241
|
+
class="px-3 py-2 text-xs rounded-lg border cursor-pointer min-h-[36px] capitalize transition-colors active:opacity-80"
|
|
242
|
+
classList={{
|
|
243
|
+
"border-amber-500/40 bg-amber-500/10 text-amber-400":
|
|
244
|
+
selected(),
|
|
245
|
+
"border-zinc-700 bg-zinc-800/50 text-zinc-400 hover:border-zinc-600":
|
|
246
|
+
!selected(),
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
All {role.label}s
|
|
250
|
+
<Show when={membersInRole().length > 0}>
|
|
251
|
+
<span class="text-zinc-600 ml-1">
|
|
252
|
+
({membersInRole().length})
|
|
253
|
+
</span>
|
|
254
|
+
</Show>
|
|
255
|
+
</button>
|
|
256
|
+
);
|
|
257
|
+
}}
|
|
258
|
+
</For>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<span class="text-xs text-zinc-500 block mb-2">
|
|
262
|
+
Or select people
|
|
263
|
+
</span>
|
|
264
|
+
<div class="space-y-0.5 max-h-[150px] overflow-y-auto">
|
|
265
|
+
<For
|
|
266
|
+
each={(Array.isArray(props.orgMembers)
|
|
267
|
+
? props.orgMembers
|
|
268
|
+
: []
|
|
269
|
+
).filter((m) => m.role !== "admin")}
|
|
270
|
+
>
|
|
271
|
+
{(m) => {
|
|
272
|
+
const coveringRole = () =>
|
|
273
|
+
props.shareableRoles.find(
|
|
274
|
+
(r) =>
|
|
275
|
+
r.code === m.role &&
|
|
276
|
+
props.sharedRoleCodes.includes(r.code)
|
|
277
|
+
);
|
|
278
|
+
const checked = () =>
|
|
279
|
+
props.sharedWith.includes(m.user_id) || !!coveringRole();
|
|
280
|
+
return (
|
|
281
|
+
<label
|
|
282
|
+
class="flex items-center gap-3 text-sm py-2 px-2 rounded-lg min-h-[40px] transition-colors"
|
|
283
|
+
classList={{
|
|
284
|
+
"text-zinc-300 cursor-pointer hover:bg-zinc-800/30 active:bg-zinc-800/50":
|
|
285
|
+
!coveringRole(),
|
|
286
|
+
"text-zinc-500 cursor-not-allowed bg-zinc-900/40":
|
|
287
|
+
!!coveringRole(),
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<div
|
|
291
|
+
class="w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors"
|
|
292
|
+
classList={{
|
|
293
|
+
"border-amber-500 bg-amber-500":
|
|
294
|
+
checked() && !coveringRole(),
|
|
295
|
+
"border-amber-500/40 bg-amber-500/40":
|
|
296
|
+
checked() && !!coveringRole(),
|
|
297
|
+
"border-zinc-600 bg-transparent": !checked(),
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
<Show when={checked()}>
|
|
301
|
+
<svg
|
|
302
|
+
class="w-3 h-3 text-zinc-900"
|
|
303
|
+
fill="none"
|
|
304
|
+
viewBox="0 0 24 24"
|
|
305
|
+
stroke="currentColor"
|
|
306
|
+
stroke-width="3"
|
|
307
|
+
>
|
|
308
|
+
<path
|
|
309
|
+
stroke-linecap="round"
|
|
310
|
+
stroke-linejoin="round"
|
|
311
|
+
d="M5 13l4 4L19 7"
|
|
312
|
+
/>
|
|
313
|
+
</svg>
|
|
314
|
+
</Show>
|
|
315
|
+
</div>
|
|
316
|
+
<input
|
|
317
|
+
type="checkbox"
|
|
318
|
+
checked={checked()}
|
|
319
|
+
disabled={!!coveringRole()}
|
|
320
|
+
onChange={(e) => {
|
|
321
|
+
if (coveringRole()) return;
|
|
322
|
+
if (e.target.checked) {
|
|
323
|
+
props.setSharedWith([
|
|
324
|
+
...props.sharedWith,
|
|
325
|
+
m.user_id,
|
|
326
|
+
]);
|
|
327
|
+
} else {
|
|
328
|
+
props.setSharedWith(
|
|
329
|
+
props.sharedWith.filter((id) => id !== m.user_id)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
class="sr-only"
|
|
334
|
+
/>
|
|
335
|
+
<span class="flex-1">{m.name}</span>
|
|
336
|
+
<Show
|
|
337
|
+
when={coveringRole()}
|
|
338
|
+
fallback={
|
|
339
|
+
<span class="text-[10px] text-zinc-600 capitalize">
|
|
340
|
+
{m.role}
|
|
341
|
+
</span>
|
|
342
|
+
}
|
|
343
|
+
>
|
|
344
|
+
<span class="text-[10px] text-amber-500/70">
|
|
345
|
+
via All {coveringRole()!.label}s
|
|
346
|
+
</span>
|
|
347
|
+
</Show>
|
|
348
|
+
</label>
|
|
349
|
+
);
|
|
350
|
+
}}
|
|
351
|
+
</For>
|
|
352
|
+
<Show
|
|
353
|
+
when={
|
|
354
|
+
!Array.isArray(props.orgMembers) ||
|
|
355
|
+
props.orgMembers.length === 0
|
|
356
|
+
}
|
|
357
|
+
>
|
|
358
|
+
<p class="text-xs text-zinc-600 py-2">Loading members...</p>
|
|
359
|
+
</Show>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</Show>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|