@dloizides/ui-card-roster 0.1.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/dist/index.js ADDED
@@ -0,0 +1,654 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactNative = require('react-native');
5
+ var uiFeedback = require('@dloizides/ui-feedback');
6
+ var uiLayout = require('@dloizides/ui-layout');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+ var uiMotion = require('@dloizides/ui-motion');
9
+
10
+ // src/components/CardRoster.tsx
11
+
12
+ // src/constants.ts
13
+ var MIN_TOUCH_TARGET = 44;
14
+ var DEFAULT_CARD_WIDTH = 168;
15
+ var SPLIT_EDITOR_WIDTH = 400;
16
+ var SPACE_XS = 4;
17
+ var SPACE_S = 8;
18
+ var SPACE_M = 12;
19
+ var SPACE_L = 16;
20
+ var RADIUS_S = 8;
21
+ var RADIUS_M = 12;
22
+ var BORDER_WIDTH = 1;
23
+ var ACTIVE_BORDER_WIDTH = 2;
24
+ var TITLE_FONT_SIZE = 17;
25
+ var BODY_FONT_SIZE = 15;
26
+ var SMALL_FONT_SIZE = 13;
27
+ var PRESSED_OPACITY = 0.7;
28
+ var DISABLED_OPACITY = 0.4;
29
+ var BUSY_OVERLAY_OPACITY = 0.9;
30
+ var GROUP_TITLE_MIN_WIDTH = 120;
31
+ var REORDER_SPRING = { stiffness: 320, damping: 32 };
32
+ var MOVE_UP = -1;
33
+ var MOVE_DOWN = 1;
34
+ var GLYPH_UP = "\u25B2";
35
+ var GLYPH_DOWN = "\u25BC";
36
+ var DEFAULT_TEST_ID = "card-roster";
37
+ var rosterTestIds = (base) => ({
38
+ root: base,
39
+ reorderToggle: `${base}-reorder-toggle`,
40
+ addGroup: `${base}-add-group`,
41
+ error: `${base}-error`,
42
+ editor: `${base}-editor`,
43
+ editorPlaceholder: `${base}-editor-placeholder`,
44
+ group: (key) => `${base}-group-${key}`,
45
+ card: (id) => `${base}-card-${id}`
46
+ });
47
+
48
+ // src/utils/rosterOps.ts
49
+ function canMove(length, index, delta) {
50
+ const target = index + delta;
51
+ return delta !== 0 && index >= 0 && index < length && target >= 0 && target < length;
52
+ }
53
+ function moveInArray(list, index, delta) {
54
+ if (!canMove(list.length, index, delta)) return list;
55
+ const next = [...list];
56
+ const [moved] = next.splice(index, 1);
57
+ next.splice(index + delta, 0, moved);
58
+ return next;
59
+ }
60
+ function isGroupEmpty(group) {
61
+ return group.items.length === 0;
62
+ }
63
+ function mapGroup(value, key, fn) {
64
+ let changed = false;
65
+ const groups = value.groups.map((group) => {
66
+ if (group.key !== key) return group;
67
+ const next = fn(group);
68
+ if (next !== group) changed = true;
69
+ return next;
70
+ });
71
+ return changed ? { ...value, groups } : value;
72
+ }
73
+ function findItem(value, itemId) {
74
+ for (const group of value.groups) {
75
+ const index = group.items.findIndex((item2) => item2.id === itemId);
76
+ const item = group.items[index];
77
+ if (item !== void 0) return { group, item, index };
78
+ }
79
+ return void 0;
80
+ }
81
+ function addGroup(value, group) {
82
+ if (value.groups.some((g) => g.key === group.key)) return value;
83
+ return { ...value, groups: [...value.groups, group] };
84
+ }
85
+ function renameGroup(value, key, title) {
86
+ const trimmed = title.trim();
87
+ if (trimmed === "") return value;
88
+ return mapGroup(value, key, (group) => group.title === trimmed ? group : { ...group, title: trimmed });
89
+ }
90
+ function removeGroup(value, key) {
91
+ if (!value.groups.some((g) => g.key === key)) return value;
92
+ return { ...value, groups: value.groups.filter((g) => g.key !== key) };
93
+ }
94
+ function moveGroup(value, key, delta) {
95
+ const index = value.groups.findIndex((g) => g.key === key);
96
+ const groups = moveInArray(value.groups, index, delta);
97
+ return groups === value.groups ? value : { ...value, groups };
98
+ }
99
+ function addItem(value, groupKey, item) {
100
+ if (findItem(value, item.id) !== void 0) return value;
101
+ return mapGroup(value, groupKey, (group) => ({ ...group, items: [...group.items, item] }));
102
+ }
103
+ function updateItem(value, groupKey, item) {
104
+ return mapGroup(value, groupKey, (group) => {
105
+ const index = group.items.findIndex((i) => i.id === item.id);
106
+ if (index === -1) return group;
107
+ const items = [...group.items];
108
+ items[index] = item;
109
+ return { ...group, items };
110
+ });
111
+ }
112
+ function removeItem(value, groupKey, itemId) {
113
+ return mapGroup(
114
+ value,
115
+ groupKey,
116
+ (group) => group.items.some((i) => i.id === itemId) ? { ...group, items: group.items.filter((i) => i.id !== itemId) } : group
117
+ );
118
+ }
119
+ function moveItem(value, groupKey, itemId, delta) {
120
+ return mapGroup(value, groupKey, (group) => {
121
+ const items = moveInArray(group.items, group.items.findIndex((i) => i.id === itemId), delta);
122
+ return items === group.items ? group : { ...group, items };
123
+ });
124
+ }
125
+
126
+ // src/hooks/useRosterActions.ts
127
+ function useRosterActions(value, onChange) {
128
+ return react.useMemo(() => {
129
+ const emit = (next) => {
130
+ if (next !== value) onChange(next);
131
+ };
132
+ return {
133
+ addGroup: (group) => emit(addGroup(value, group)),
134
+ renameGroup: (key, title) => emit(renameGroup(value, key, title)),
135
+ removeGroup: (key) => emit(removeGroup(value, key)),
136
+ moveGroup: (key, delta) => emit(moveGroup(value, key, delta)),
137
+ moveItem: (groupKey, itemId, delta) => emit(moveItem(value, groupKey, itemId, delta))
138
+ };
139
+ }, [value, onChange]);
140
+ }
141
+
142
+ // src/utils/editorSession.ts
143
+ function isSplitPane(width, breakpoint) {
144
+ return width >= breakpoint;
145
+ }
146
+ function resolveEditSession(value, target, draft) {
147
+ if (target === null) return null;
148
+ if (target.itemId === null) return draft === null ? null : { item: draft, groupKey: target.groupKey, isNew: true };
149
+ const found = findItem(value, target.itemId);
150
+ return found === void 0 ? null : { item: found.item, groupKey: found.group.key, isNew: false };
151
+ }
152
+ function commitEdit(value, session, next) {
153
+ return session.isNew ? addItem(value, session.groupKey, next) : updateItem(value, session.groupKey, next);
154
+ }
155
+
156
+ // src/hooks/useRosterEditor.ts
157
+ function useRosterEditor(value, onChange, createItem) {
158
+ const [target, setTarget] = react.useState(null);
159
+ const [draft, setDraft] = react.useState(null);
160
+ const close = react.useCallback(() => {
161
+ setTarget(null);
162
+ setDraft(null);
163
+ }, []);
164
+ const openEdit = react.useCallback((groupKey, itemId) => {
165
+ setDraft(null);
166
+ setTarget({ groupKey, itemId });
167
+ }, []);
168
+ const openNew = react.useCallback(
169
+ (groupKey) => {
170
+ if (createItem === void 0) return;
171
+ setDraft(createItem(groupKey));
172
+ setTarget({ groupKey, itemId: null });
173
+ },
174
+ [createItem]
175
+ );
176
+ const session = resolveEditSession(value, target, draft);
177
+ const context = session === null ? null : {
178
+ ...session,
179
+ save: (next) => {
180
+ const updated = commitEdit(value, session, next);
181
+ if (updated !== value) onChange(updated);
182
+ close();
183
+ },
184
+ cancel: close,
185
+ remove: () => {
186
+ if (!session.isNew) onChange(removeItem(value, session.groupKey, session.item.id));
187
+ close();
188
+ }
189
+ };
190
+ const activeItemId = session !== null && !session.isNew ? session.item.id : null;
191
+ return { context, activeItemId, openEdit, openNew, close };
192
+ }
193
+ var styles = reactNative.StyleSheet.create({
194
+ pane: { width: SPLIT_EDITOR_WIDTH, borderWidth: BORDER_WIDTH, borderRadius: RADIUS_M, padding: SPACE_L, gap: SPACE_L },
195
+ title: { fontSize: TITLE_FONT_SIZE, fontWeight: "700" },
196
+ prompt: { fontSize: BODY_FONT_SIZE }
197
+ });
198
+ function EditorPane({
199
+ context,
200
+ renderEditor,
201
+ split,
202
+ labels,
203
+ onClose,
204
+ testID,
205
+ placeholderTestID
206
+ }) {
207
+ const { theme } = uiFeedback.useUi();
208
+ const title = context?.isNew === true ? labels.editorTitleNew : labels.editorTitleEdit;
209
+ if (!split)
210
+ return /* @__PURE__ */ jsxRuntime.jsx(uiLayout.ModalShell, { title, visible: context !== null, onCancel: onClose, children: context === null ? null : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID, children: renderEditor(context) }) });
211
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.pane, { borderColor: theme.colors.border, backgroundColor: theme.colors.surface }], testID, children: context === null ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.prompt, { color: theme.colors.textSecondary }], testID: placeholderTestID, children: labels.selectPrompt }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
212
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [styles.title, { color: theme.colors.text }], children: title }),
213
+ renderEditor(context)
214
+ ] }) });
215
+ }
216
+ var styles2 = reactNative.StyleSheet.create({
217
+ base: {
218
+ minHeight: MIN_TOUCH_TARGET,
219
+ minWidth: MIN_TOUCH_TARGET,
220
+ paddingHorizontal: SPACE_M,
221
+ borderRadius: RADIUS_S,
222
+ borderWidth: BORDER_WIDTH,
223
+ alignItems: "center",
224
+ justifyContent: "center"
225
+ },
226
+ label: { fontSize: BODY_FONT_SIZE, fontWeight: "600" },
227
+ pressed: { opacity: PRESSED_OPACITY },
228
+ disabled: { opacity: DISABLED_OPACITY }
229
+ });
230
+ function RosterButton({
231
+ label,
232
+ accessibilityLabel,
233
+ hint,
234
+ onPress,
235
+ testID,
236
+ emphasis = false,
237
+ danger = false,
238
+ disabled = false
239
+ }) {
240
+ const { theme } = uiFeedback.useUi();
241
+ const accent = danger ? theme.semantic.error["500"] : theme.palette.primary["500"];
242
+ const tone = emphasis ? { backgroundColor: accent, borderColor: accent } : { borderColor: theme.colors.border };
243
+ const color = emphasis ? theme.colors.background : accent;
244
+ return /* @__PURE__ */ jsxRuntime.jsx(
245
+ reactNative.Pressable,
246
+ {
247
+ accessibilityHint: hint,
248
+ accessibilityLabel: accessibilityLabel ?? label,
249
+ accessibilityRole: "button",
250
+ accessibilityState: { disabled },
251
+ disabled,
252
+ style: ({ pressed }) => [styles2.base, tone, pressed ? styles2.pressed : null, disabled ? styles2.disabled : null],
253
+ testID,
254
+ onPress,
255
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label, { color }], children: label })
256
+ }
257
+ );
258
+ }
259
+ var styles3 = reactNative.StyleSheet.create({
260
+ frame: { borderRadius: RADIUS_M, padding: SPACE_S, gap: SPACE_S, overflow: "hidden" },
261
+ fill: { alignSelf: "stretch" },
262
+ body: { position: "relative" },
263
+ busy: {
264
+ ...reactNative.StyleSheet.absoluteFillObject,
265
+ alignItems: "center",
266
+ justifyContent: "center",
267
+ gap: SPACE_S,
268
+ opacity: BUSY_OVERLAY_OPACITY
269
+ },
270
+ small: { fontSize: SMALL_FONT_SIZE },
271
+ errorRow: { gap: SPACE_S },
272
+ footer: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_S }
273
+ });
274
+ function ItemFrame({
275
+ width,
276
+ active,
277
+ state,
278
+ labels,
279
+ onRetry,
280
+ testID,
281
+ children,
282
+ footer
283
+ }) {
284
+ const { theme } = uiFeedback.useUi();
285
+ const busy = state?.busy === true;
286
+ const error = state?.error ?? "";
287
+ const hasError = error !== "";
288
+ const errorColor = theme.semantic.error["500"];
289
+ const primary = theme.palette.primary["500"];
290
+ const highlighted = active || hasError;
291
+ const borderColor = hasError ? errorColor : active ? primary : theme.colors.border;
292
+ return /* @__PURE__ */ jsxRuntime.jsxs(
293
+ reactNative.View,
294
+ {
295
+ accessibilityState: { busy },
296
+ style: [
297
+ styles3.frame,
298
+ width === void 0 ? styles3.fill : { width },
299
+ { borderColor, backgroundColor: theme.colors.surface, borderWidth: highlighted ? ACTIVE_BORDER_WIDTH : BORDER_WIDTH }
300
+ ],
301
+ testID,
302
+ children: [
303
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.body, children: [
304
+ children,
305
+ busy ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles3.busy, { backgroundColor: theme.colors.surfaceElevated }], testID: `${testID}-busy`, children: [
306
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary }),
307
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityLiveRegion: "polite", style: [styles3.small, { color: theme.colors.textSecondary }], children: labels.uploading })
308
+ ] }) : null
309
+ ] }),
310
+ hasError ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.errorRow, testID: `${testID}-error`, children: [
311
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "alert", style: [styles3.small, { color: errorColor }], children: error }),
312
+ onRetry !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(RosterButton, { danger: true, hint: labels.retryHint, label: labels.retry, testID: `${testID}-retry`, onPress: onRetry }) : null
313
+ ] }) : null,
314
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.footer, children: footer })
315
+ ]
316
+ }
317
+ );
318
+ }
319
+ var styles4 = reactNative.StyleSheet.create({
320
+ band: { gap: SPACE_M, paddingVertical: SPACE_XS }
321
+ });
322
+ function CardBand({ group, shared }) {
323
+ const { labels, ids, onRetryItem } = shared;
324
+ return /* @__PURE__ */ jsxRuntime.jsx(
325
+ reactNative.ScrollView,
326
+ {
327
+ horizontal: true,
328
+ contentContainerStyle: styles4.band,
329
+ showsHorizontalScrollIndicator: false,
330
+ testID: `${ids.group(group.key)}-band`,
331
+ children: group.items.map((item) => {
332
+ const cardId = ids.card(item.id);
333
+ return /* @__PURE__ */ jsxRuntime.jsx(
334
+ ItemFrame,
335
+ {
336
+ active: shared.activeItemId === item.id,
337
+ footer: /* @__PURE__ */ jsxRuntime.jsx(
338
+ RosterButton,
339
+ {
340
+ accessibilityLabel: labels.editCard(shared.getItemName(item)),
341
+ hint: labels.editCardHint,
342
+ label: labels.edit,
343
+ testID: `${cardId}-edit`,
344
+ onPress: () => shared.onEdit(group.key, item.id)
345
+ }
346
+ ),
347
+ labels,
348
+ state: shared.getItemState?.(item),
349
+ testID: cardId,
350
+ width: shared.cardWidth,
351
+ onRetry: onRetryItem === void 0 ? void 0 : () => onRetryItem(item),
352
+ children: shared.renderCard(item, group)
353
+ },
354
+ item.id
355
+ );
356
+ })
357
+ }
358
+ );
359
+ }
360
+ var styles5 = reactNative.StyleSheet.create({
361
+ header: { flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: SPACE_S },
362
+ title: { flexGrow: 1, flexShrink: 1, minWidth: GROUP_TITLE_MIN_WIDTH, fontSize: TITLE_FONT_SIZE, fontWeight: "700" },
363
+ input: {
364
+ flexGrow: 1,
365
+ minWidth: GROUP_TITLE_MIN_WIDTH,
366
+ minHeight: MIN_TOUCH_TARGET,
367
+ paddingHorizontal: SPACE_M,
368
+ borderWidth: BORDER_WIDTH,
369
+ borderRadius: RADIUS_S,
370
+ fontSize: BODY_FONT_SIZE
371
+ },
372
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_S }
373
+ });
374
+ function GroupHeader({ group, index, shared }) {
375
+ const { theme } = uiFeedback.useUi();
376
+ const [draft, setDraft] = react.useState(null);
377
+ const { labels, actions, reordering } = shared;
378
+ const gid = shared.ids.group(group.key);
379
+ const commit = () => {
380
+ if (draft !== null) actions.renameGroup(group.key, draft);
381
+ setDraft(null);
382
+ };
383
+ const reorderActions = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
384
+ /* @__PURE__ */ jsxRuntime.jsx(
385
+ RosterButton,
386
+ {
387
+ accessibilityLabel: labels.moveGroupUp(group.title),
388
+ disabled: index === 0,
389
+ hint: labels.moveGroupHint,
390
+ label: GLYPH_UP,
391
+ testID: `${gid}-up`,
392
+ onPress: () => actions.moveGroup(group.key, MOVE_UP)
393
+ }
394
+ ),
395
+ /* @__PURE__ */ jsxRuntime.jsx(
396
+ RosterButton,
397
+ {
398
+ accessibilityLabel: labels.moveGroupDown(group.title),
399
+ disabled: index === shared.groupCount - 1,
400
+ hint: labels.moveGroupHint,
401
+ label: GLYPH_DOWN,
402
+ testID: `${gid}-down`,
403
+ onPress: () => actions.moveGroup(group.key, MOVE_DOWN)
404
+ }
405
+ )
406
+ ] });
407
+ const editActions = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
408
+ shared.onAdd !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
409
+ RosterButton,
410
+ {
411
+ emphasis: true,
412
+ accessibilityLabel: labels.addCard(group.title),
413
+ hint: labels.addCardHint,
414
+ label: labels.add,
415
+ testID: `${gid}-add`,
416
+ onPress: () => shared.onAdd?.(group.key)
417
+ }
418
+ ) : null,
419
+ /* @__PURE__ */ jsxRuntime.jsx(
420
+ RosterButton,
421
+ {
422
+ accessibilityLabel: labels.renameGroup(group.title),
423
+ hint: labels.renameGroupHint,
424
+ label: labels.rename,
425
+ testID: `${gid}-rename`,
426
+ onPress: () => setDraft(group.title)
427
+ }
428
+ ),
429
+ shared.canRemoveGroup(group) ? /* @__PURE__ */ jsxRuntime.jsx(
430
+ RosterButton,
431
+ {
432
+ danger: true,
433
+ accessibilityLabel: labels.removeGroup(group.title),
434
+ hint: labels.removeGroupHint,
435
+ label: labels.remove,
436
+ testID: `${gid}-remove`,
437
+ onPress: () => actions.removeGroup(group.key)
438
+ }
439
+ ) : null
440
+ ] });
441
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles5.header, children: [
442
+ draft === null ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", numberOfLines: 1, style: [styles5.title, { color: theme.colors.text }], children: group.title }) : /* @__PURE__ */ jsxRuntime.jsx(
443
+ reactNative.TextInput,
444
+ {
445
+ autoFocus: true,
446
+ accessibilityHint: labels.groupTitleInputHint,
447
+ accessibilityLabel: labels.groupTitleInput,
448
+ style: [styles5.input, { color: theme.colors.text, borderColor: theme.palette.primary["500"] }],
449
+ testID: `${gid}-title-input`,
450
+ value: draft,
451
+ onBlur: commit,
452
+ onChangeText: setDraft,
453
+ onSubmitEditing: commit
454
+ }
455
+ ),
456
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles5.actions, children: reordering ? reorderActions : editActions })
457
+ ] });
458
+ }
459
+ var styles6 = reactNative.StyleSheet.create({
460
+ item: { paddingVertical: SPACE_S / 2 }
461
+ });
462
+ function ReorderList({ group, shared }) {
463
+ const { labels, ids, actions } = shared;
464
+ const keys = react.useMemo(() => group.items.map((item) => item.id), [group.items]);
465
+ const last = group.items.length - 1;
466
+ const renderItem = (key) => {
467
+ const index = group.items.findIndex((item2) => item2.id === key);
468
+ const item = group.items[index];
469
+ if (item === void 0) return null;
470
+ const name = shared.getItemName(item);
471
+ const cardId = ids.card(item.id);
472
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles6.item, children: /* @__PURE__ */ jsxRuntime.jsx(
473
+ ItemFrame,
474
+ {
475
+ active: false,
476
+ footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
477
+ /* @__PURE__ */ jsxRuntime.jsx(
478
+ RosterButton,
479
+ {
480
+ accessibilityLabel: labels.moveCardUp(name),
481
+ disabled: index === 0,
482
+ hint: labels.moveCardHint,
483
+ label: GLYPH_UP,
484
+ testID: `${cardId}-up`,
485
+ onPress: () => actions.moveItem(group.key, item.id, MOVE_UP)
486
+ }
487
+ ),
488
+ /* @__PURE__ */ jsxRuntime.jsx(
489
+ RosterButton,
490
+ {
491
+ accessibilityLabel: labels.moveCardDown(name),
492
+ disabled: index === last,
493
+ hint: labels.moveCardHint,
494
+ label: GLYPH_DOWN,
495
+ testID: `${cardId}-down`,
496
+ onPress: () => actions.moveItem(group.key, item.id, MOVE_DOWN)
497
+ }
498
+ )
499
+ ] }),
500
+ labels,
501
+ state: shared.getItemState?.(item),
502
+ testID: cardId,
503
+ children: shared.renderCard(item, group)
504
+ }
505
+ ) });
506
+ };
507
+ return /* @__PURE__ */ jsxRuntime.jsx(uiMotion.Reorder, { itemKeys: keys, spring: REORDER_SPRING, testID: `${ids.group(group.key)}-reorder`, children: renderItem });
508
+ }
509
+ var styles7 = reactNative.StyleSheet.create({
510
+ section: { gap: SPACE_S },
511
+ empty: {
512
+ borderWidth: BORDER_WIDTH,
513
+ borderStyle: "dashed",
514
+ borderRadius: RADIUS_M,
515
+ padding: SPACE_L,
516
+ alignItems: "center"
517
+ },
518
+ emptyText: { fontSize: BODY_FONT_SIZE, textAlign: "center" }
519
+ });
520
+ function RosterGroupSection({
521
+ group,
522
+ index,
523
+ shared
524
+ }) {
525
+ const { theme } = uiFeedback.useUi();
526
+ const gid = shared.ids.group(group.key);
527
+ let body;
528
+ if (group.items.length === 0)
529
+ body = /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.empty, { borderColor: theme.colors.border }], testID: `${gid}-empty`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.emptyText, { color: theme.colors.textSecondary }], children: shared.labels.emptyGroup }) });
530
+ else if (shared.reordering) body = /* @__PURE__ */ jsxRuntime.jsx(ReorderList, { group, shared });
531
+ else body = /* @__PURE__ */ jsxRuntime.jsx(CardBand, { group, shared });
532
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.section, testID: gid, children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx(GroupHeader, { group, index, shared }),
534
+ body
535
+ ] });
536
+ }
537
+ var styles8 = reactNative.StyleSheet.create({
538
+ root: { gap: SPACE_L },
539
+ split: { flexDirection: "row", alignItems: "flex-start" },
540
+ list: { flex: 1, minWidth: 0, gap: SPACE_L },
541
+ toolbar: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_S, justifyContent: "flex-end" }
542
+ });
543
+ function CardRoster(props) {
544
+ const {
545
+ value,
546
+ onChange,
547
+ renderCard,
548
+ renderEditor,
549
+ getItemName,
550
+ labels,
551
+ createItem,
552
+ createGroup,
553
+ canRemoveGroup = isGroupEmpty,
554
+ getItemState,
555
+ onRetryItem,
556
+ error,
557
+ wideBreakpoint = uiLayout.LAYOUT_COLLAPSE_BREAKPOINT,
558
+ cardWidth = DEFAULT_CARD_WIDTH,
559
+ testID = DEFAULT_TEST_ID
560
+ } = props;
561
+ const { width } = reactNative.useWindowDimensions();
562
+ const split = isSplitPane(width, wideBreakpoint);
563
+ const [reordering, setReordering] = react.useState(false);
564
+ const editor = useRosterEditor(value, onChange, createItem);
565
+ const actions = useRosterActions(value, onChange);
566
+ const ids = react.useMemo(() => rosterTestIds(testID), [testID]);
567
+ const { close, openEdit, openNew } = editor;
568
+ const toggleReorder = react.useCallback(() => {
569
+ close();
570
+ setReordering((on) => !on);
571
+ }, [close]);
572
+ const shared = {
573
+ renderCard,
574
+ getItemName,
575
+ getItemState,
576
+ onRetryItem,
577
+ labels,
578
+ cardWidth,
579
+ ids,
580
+ reordering,
581
+ actions,
582
+ canRemoveGroup,
583
+ activeItemId: editor.activeItemId,
584
+ groupCount: value.groups.length,
585
+ onEdit: openEdit,
586
+ onAdd: createItem === void 0 ? void 0 : openNew
587
+ };
588
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles8.root, split ? styles8.split : null], testID: ids.root, children: [
589
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.list, children: [
590
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.toolbar, children: [
591
+ /* @__PURE__ */ jsxRuntime.jsx(
592
+ RosterButton,
593
+ {
594
+ emphasis: reordering,
595
+ hint: reordering ? labels.doneReorderingHint : labels.reorderHint,
596
+ label: reordering ? labels.doneReordering : labels.reorder,
597
+ testID: ids.reorderToggle,
598
+ onPress: toggleReorder
599
+ }
600
+ ),
601
+ createGroup !== void 0 && !reordering ? /* @__PURE__ */ jsxRuntime.jsx(
602
+ RosterButton,
603
+ {
604
+ hint: labels.addGroupHint,
605
+ label: labels.addGroup,
606
+ testID: ids.addGroup,
607
+ onPress: () => actions.addGroup(createGroup())
608
+ }
609
+ ) : null
610
+ ] }),
611
+ error !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(uiFeedback.ErrorState, { message: error.message, testID: ids.error, title: error.title, onRetry: error.onRetry }) : null,
612
+ value.groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(RosterGroupSection, { group, index, shared }, group.key))
613
+ ] }),
614
+ reordering && split ? null : /* @__PURE__ */ jsxRuntime.jsx(
615
+ EditorPane,
616
+ {
617
+ context: editor.context,
618
+ labels,
619
+ placeholderTestID: ids.editorPlaceholder,
620
+ renderEditor,
621
+ split,
622
+ testID: ids.editor,
623
+ onClose: close
624
+ }
625
+ )
626
+ ] });
627
+ }
628
+
629
+ exports.CardRoster = CardRoster;
630
+ exports.DEFAULT_CARD_WIDTH = DEFAULT_CARD_WIDTH;
631
+ exports.DEFAULT_TEST_ID = DEFAULT_TEST_ID;
632
+ exports.MIN_TOUCH_TARGET = MIN_TOUCH_TARGET;
633
+ exports.MOVE_DOWN = MOVE_DOWN;
634
+ exports.MOVE_UP = MOVE_UP;
635
+ exports.addGroup = addGroup;
636
+ exports.addItem = addItem;
637
+ exports.canMove = canMove;
638
+ exports.commitEdit = commitEdit;
639
+ exports.findItem = findItem;
640
+ exports.isGroupEmpty = isGroupEmpty;
641
+ exports.isSplitPane = isSplitPane;
642
+ exports.moveGroup = moveGroup;
643
+ exports.moveInArray = moveInArray;
644
+ exports.moveItem = moveItem;
645
+ exports.removeGroup = removeGroup;
646
+ exports.removeItem = removeItem;
647
+ exports.renameGroup = renameGroup;
648
+ exports.resolveEditSession = resolveEditSession;
649
+ exports.rosterTestIds = rosterTestIds;
650
+ exports.updateItem = updateItem;
651
+ exports.useRosterActions = useRosterActions;
652
+ exports.useRosterEditor = useRosterEditor;
653
+ //# sourceMappingURL=index.js.map
654
+ //# sourceMappingURL=index.js.map