@simplysm/solid 13.0.96 → 13.0.98

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.
@@ -0,0 +1,537 @@
1
+ # Form Controls
2
+
3
+ Source: `src/components/form-control/**/*.tsx`
4
+
5
+ ## Button
6
+
7
+ Themed button with ripple effect. Extends `JSX.ButtonHTMLAttributes<HTMLButtonElement>`.
8
+
9
+ ```ts
10
+ interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
11
+ theme?: SemanticTheme; // "primary" | "info" | "success" | "warning" | "danger" | "base" (default: "base")
12
+ variant?: "solid" | "outline" | "ghost"; // default: "outline"
13
+ size?: ComponentSize; // "xs" | "sm" | "md" | "lg" | "xl" (default: "md")
14
+ inset?: boolean; // borderless style
15
+ }
16
+ ```
17
+
18
+ ## Select
19
+
20
+ Single or multiple select dropdown. Supports two modes: `items` prop mode and `children` mode.
21
+
22
+ ```ts
23
+ // Common props
24
+ interface SelectCommonProps<TValue> {
25
+ disabled?: boolean;
26
+ required?: boolean;
27
+ placeholder?: string;
28
+ size?: ComponentSize;
29
+ inset?: boolean;
30
+ validate?: (value: unknown) => string | undefined;
31
+ lazyValidation?: boolean;
32
+ itemSearchText?: (item: TValue) => string; // enables search input
33
+ isItemHidden?: (item: TValue) => boolean;
34
+ class?: string;
35
+ style?: JSX.CSSProperties;
36
+ }
37
+
38
+ // Single select
39
+ interface SelectSingleProps<TValue> extends SelectCommonProps<TValue> {
40
+ multiple?: false;
41
+ value?: TValue;
42
+ onValueChange?: (value: TValue | undefined) => void;
43
+ }
44
+
45
+ // Multiple select
46
+ interface SelectMultipleProps<TValue> extends SelectCommonProps<TValue> {
47
+ multiple: true;
48
+ value?: TValue[];
49
+ onValueChange?: (value: TValue[]) => void;
50
+ tagDirection?: "horizontal" | "vertical";
51
+ hideSelectAll?: boolean;
52
+ }
53
+
54
+ // Items mode: pass items array + ItemTemplate
55
+ // Children mode: pass children + renderValue
56
+ ```
57
+
58
+ ### Sub-components
59
+
60
+ - **`Select.Item<TValue>`** -- Selectable item. Props: `value: TValue`, `disabled?: boolean`. Supports nested items via `Select.Item.Children`.
61
+ - **`Select.Header`** -- Custom header slot rendered above dropdown list.
62
+ - **`Select.Action`** -- Action button appended to the trigger. Extends button attributes.
63
+ - **`Select.ItemTemplate`** -- Render template for `items` mode. Child is a function: `(item, index, depth) => JSX.Element`.
64
+
65
+ ## Combobox
66
+
67
+ Autocomplete component with async search and debounced loading.
68
+
69
+ ```ts
70
+ interface ComboboxBaseProps<TValue> {
71
+ value?: TValue;
72
+ onValueChange?: (value: TValue) => void;
73
+ loadItems: (query: string) => TValue[] | Promise<TValue[]>; // required
74
+ debounceMs?: number; // default: 300
75
+ renderValue: (value: TValue) => JSX.Element; // required
76
+ disabled?: boolean;
77
+ required?: boolean;
78
+ validate?: (value: TValue | undefined) => string | undefined;
79
+ lazyValidation?: boolean;
80
+ placeholder?: string;
81
+ size?: ComponentSize;
82
+ inset?: boolean;
83
+ class?: string;
84
+ style?: JSX.CSSProperties;
85
+ children?: JSX.Element;
86
+ }
87
+
88
+ // Custom value variants
89
+ interface ComboboxCustomParserProps<TValue> extends ComboboxBaseProps<TValue> {
90
+ allowsCustomValue: true;
91
+ parseCustomValue: (text: string) => TValue;
92
+ }
93
+
94
+ interface ComboboxCustomStringProps extends ComboboxBaseProps<string> {
95
+ allowsCustomValue: true;
96
+ parseCustomValue?: undefined; // TValue must be string
97
+ }
98
+
99
+ interface ComboboxDefaultProps<TValue> extends ComboboxBaseProps<TValue> {
100
+ allowsCustomValue?: false;
101
+ }
102
+ ```
103
+
104
+ ### Sub-components
105
+
106
+ - **`Combobox.Item<TValue>`** -- Selectable item. Props: `value: TValue`, `disabled?: boolean`.
107
+ - **`Combobox.ItemTemplate`** -- Render template for loaded items. Child is a function: `(item, index) => JSX.Element`.
108
+
109
+ ## TextInput
110
+
111
+ Text input field with format support and IME handling.
112
+
113
+ ```ts
114
+ interface TextInputProps {
115
+ value?: string;
116
+ onValueChange?: (value: string) => void;
117
+ type?: "text" | "password" | "email";
118
+ placeholder?: string;
119
+ title?: string;
120
+ autocomplete?: JSX.HTMLAutocomplete;
121
+ disabled?: boolean;
122
+ readOnly?: boolean;
123
+ size?: ComponentSize;
124
+ inset?: boolean;
125
+ format?: string; // e.g., "XXX-XXXX-XXXX"
126
+ required?: boolean;
127
+ minLength?: number;
128
+ maxLength?: number;
129
+ pattern?: string | RegExp;
130
+ validate?: (value: string) => string | undefined;
131
+ lazyValidation?: boolean;
132
+ class?: string;
133
+ style?: JSX.CSSProperties;
134
+ children?: JSX.Element; // TextInput.Prefix slot
135
+ }
136
+ ```
137
+
138
+ ### Sub-components
139
+
140
+ - **`TextInput.Prefix`** -- Prefix content displayed before the input.
141
+
142
+ ## NumberInput
143
+
144
+ Numeric input with thousand separator and decimal formatting.
145
+
146
+ ```ts
147
+ interface NumberInputProps {
148
+ value?: number;
149
+ onValueChange?: (value: number | undefined) => void;
150
+ useGrouping?: boolean; // default: true
151
+ minimumFractionDigits?: number;
152
+ placeholder?: string;
153
+ title?: string;
154
+ disabled?: boolean;
155
+ readOnly?: boolean;
156
+ size?: ComponentSize;
157
+ inset?: boolean;
158
+ class?: string;
159
+ style?: JSX.CSSProperties;
160
+ required?: boolean;
161
+ min?: number;
162
+ max?: number;
163
+ validate?: (value: number | undefined) => string | undefined;
164
+ lazyValidation?: boolean;
165
+ children?: JSX.Element; // NumberInput.Prefix slot
166
+ }
167
+ ```
168
+
169
+ ### Sub-components
170
+
171
+ - **`NumberInput.Prefix`** -- Prefix content displayed before the input.
172
+
173
+ ## DatePicker
174
+
175
+ Date input supporting year, month, and date units. Uses `DateOnly` from `@simplysm/core-common`.
176
+
177
+ ```ts
178
+ type DatePickerUnit = "year" | "month" | "date";
179
+
180
+ interface DatePickerProps {
181
+ value?: DateOnly;
182
+ onValueChange?: (value: DateOnly | undefined) => void;
183
+ unit?: DatePickerUnit; // default: "date"
184
+ min?: DateOnly;
185
+ max?: DateOnly;
186
+ title?: string;
187
+ disabled?: boolean;
188
+ readOnly?: boolean;
189
+ size?: ComponentSize;
190
+ inset?: boolean;
191
+ class?: string;
192
+ style?: JSX.CSSProperties;
193
+ required?: boolean;
194
+ validate?: (value: DateOnly | undefined) => string | undefined;
195
+ lazyValidation?: boolean;
196
+ }
197
+ ```
198
+
199
+ ## DateTimePicker
200
+
201
+ DateTime input supporting minute and second units. Uses `DateTime` from `@simplysm/core-common`.
202
+
203
+ ```ts
204
+ type DateTimePickerUnit = "minute" | "second";
205
+
206
+ interface DateTimePickerProps {
207
+ value?: DateTime;
208
+ onValueChange?: (value: DateTime | undefined) => void;
209
+ unit?: DateTimePickerUnit; // default: "minute"
210
+ min?: DateTime;
211
+ max?: DateTime;
212
+ title?: string;
213
+ disabled?: boolean;
214
+ readOnly?: boolean;
215
+ size?: ComponentSize;
216
+ inset?: boolean;
217
+ class?: string;
218
+ style?: JSX.CSSProperties;
219
+ required?: boolean;
220
+ validate?: (value: DateTime | undefined) => string | undefined;
221
+ lazyValidation?: boolean;
222
+ }
223
+ ```
224
+
225
+ ## TimePicker
226
+
227
+ Time input supporting minute and second units. Uses `Time` from `@simplysm/core-common`.
228
+
229
+ ```ts
230
+ type TimePickerUnit = "minute" | "second";
231
+
232
+ interface TimePickerProps {
233
+ value?: Time;
234
+ onValueChange?: (value: Time | undefined) => void;
235
+ unit?: TimePickerUnit; // default: "minute"
236
+ title?: string;
237
+ disabled?: boolean;
238
+ readOnly?: boolean;
239
+ size?: ComponentSize;
240
+ inset?: boolean;
241
+ class?: string;
242
+ style?: JSX.CSSProperties;
243
+ min?: Time;
244
+ max?: Time;
245
+ required?: boolean;
246
+ validate?: (value: Time | undefined) => string | undefined;
247
+ lazyValidation?: boolean;
248
+ }
249
+ ```
250
+
251
+ ## Textarea
252
+
253
+ Auto-resizing textarea with IME handling.
254
+
255
+ ```ts
256
+ interface TextareaProps {
257
+ value?: string;
258
+ onValueChange?: (value: string) => void;
259
+ placeholder?: string;
260
+ title?: string;
261
+ disabled?: boolean;
262
+ readOnly?: boolean;
263
+ size?: ComponentSize;
264
+ inset?: boolean;
265
+ minRows?: number; // default: 1
266
+ required?: boolean;
267
+ minLength?: number;
268
+ maxLength?: number;
269
+ validate?: (value: string) => string | undefined;
270
+ lazyValidation?: boolean;
271
+ class?: string;
272
+ style?: JSX.CSSProperties;
273
+ }
274
+ ```
275
+
276
+ ## Checkbox
277
+
278
+ Checkbox with check indicator. Renders via `SelectableBase`.
279
+
280
+ ```ts
281
+ interface CheckboxProps {
282
+ checked?: boolean;
283
+ onCheckedChange?: (checked: boolean) => void;
284
+ disabled?: boolean;
285
+ size?: ComponentSize;
286
+ inset?: boolean;
287
+ inline?: boolean;
288
+ required?: boolean;
289
+ validate?: (checked: boolean) => string | undefined;
290
+ lazyValidation?: boolean;
291
+ class?: string;
292
+ style?: JSX.CSSProperties;
293
+ children?: JSX.Element;
294
+ }
295
+ ```
296
+
297
+ ## Radio
298
+
299
+ Radio button with dot indicator. Always selects on click (never deselects).
300
+
301
+ ```ts
302
+ interface RadioProps {
303
+ checked?: boolean;
304
+ onCheckedChange?: (checked: boolean) => void;
305
+ disabled?: boolean;
306
+ size?: ComponentSize;
307
+ inset?: boolean;
308
+ inline?: boolean;
309
+ required?: boolean;
310
+ validate?: (checked: boolean) => string | undefined;
311
+ lazyValidation?: boolean;
312
+ class?: string;
313
+ style?: JSX.CSSProperties;
314
+ children?: JSX.Element;
315
+ }
316
+ ```
317
+
318
+ ## CheckboxGroup
319
+
320
+ Multi-select group of checkboxes.
321
+
322
+ ```ts
323
+ interface CheckboxGroupProps<TValue> {
324
+ value?: TValue[];
325
+ onValueChange?: (value: TValue[]) => void;
326
+ disabled?: boolean;
327
+ size?: ComponentSize;
328
+ inline?: boolean;
329
+ inset?: boolean;
330
+ required?: boolean;
331
+ validate?: (value: TValue[]) => string | undefined;
332
+ lazyValidation?: boolean;
333
+ class?: string;
334
+ style?: JSX.CSSProperties;
335
+ children?: JSX.Element;
336
+ }
337
+ ```
338
+
339
+ ### Sub-components
340
+
341
+ - **`CheckboxGroup.Item<TValue>`** -- Props: `value: TValue`, `disabled?: boolean`, `children?: JSX.Element`.
342
+
343
+ ## RadioGroup
344
+
345
+ Single-select group of radio buttons.
346
+
347
+ ```ts
348
+ interface RadioGroupProps<TValue> {
349
+ value?: TValue;
350
+ onValueChange?: (value: TValue) => void;
351
+ disabled?: boolean;
352
+ size?: ComponentSize;
353
+ inline?: boolean;
354
+ inset?: boolean;
355
+ required?: boolean;
356
+ validate?: (value: TValue | undefined) => string | undefined;
357
+ lazyValidation?: boolean;
358
+ class?: string;
359
+ style?: JSX.CSSProperties;
360
+ children?: JSX.Element;
361
+ }
362
+ ```
363
+
364
+ ### Sub-components
365
+
366
+ - **`RadioGroup.Item<TValue>`** -- Props: `value: TValue`, `disabled?: boolean`, `children?: JSX.Element`.
367
+
368
+ ## ColorPicker
369
+
370
+ Native HTML color picker input.
371
+
372
+ ```ts
373
+ interface ColorPickerProps {
374
+ value?: string; // #RRGGBB format
375
+ onValueChange?: (value: string | undefined) => void;
376
+ title?: string;
377
+ disabled?: boolean;
378
+ size?: ComponentSize;
379
+ inset?: boolean;
380
+ required?: boolean;
381
+ validate?: (value: string | undefined) => string | undefined;
382
+ lazyValidation?: boolean;
383
+ class?: string;
384
+ style?: JSX.CSSProperties;
385
+ }
386
+ ```
387
+
388
+ ## DateRangePicker
389
+
390
+ Date range input with period type selector (day/month/range).
391
+
392
+ ```ts
393
+ type DateRangePeriodType = "day" | "month" | "range";
394
+
395
+ interface DateRangePickerProps {
396
+ periodType?: DateRangePeriodType;
397
+ onPeriodTypeChange?: (value: DateRangePeriodType) => void;
398
+ from?: DateOnly;
399
+ onFromChange?: (value: DateOnly | undefined) => void;
400
+ to?: DateOnly;
401
+ onToChange?: (value: DateOnly | undefined) => void;
402
+ required?: boolean;
403
+ disabled?: boolean;
404
+ size?: ComponentSize;
405
+ class?: string;
406
+ style?: JSX.CSSProperties;
407
+ }
408
+ ```
409
+
410
+ ## RichTextEditor
411
+
412
+ Tiptap-based rich text editor with toolbar (text formatting, alignment, colors, tables, images).
413
+
414
+ ```ts
415
+ interface RichTextEditorProps {
416
+ value?: string; // HTML string
417
+ onValueChange?: (value: string) => void;
418
+ disabled?: boolean;
419
+ size?: ComponentSize;
420
+ class?: string;
421
+ style?: JSX.CSSProperties;
422
+ }
423
+ ```
424
+
425
+ ## Numpad
426
+
427
+ Numeric keypad for touch-friendly number input.
428
+
429
+ ```ts
430
+ interface NumpadProps {
431
+ value?: number;
432
+ onValueChange?: (value: number | undefined) => void;
433
+ placeholder?: string;
434
+ required?: boolean;
435
+ inputDisabled?: boolean; // disable direct text field input
436
+ withEnterButton?: boolean;
437
+ withMinusButton?: boolean;
438
+ onEnterButtonClick?: () => void;
439
+ size?: ComponentSize;
440
+ class?: string;
441
+ style?: JSX.CSSProperties;
442
+ }
443
+ ```
444
+
445
+ ## StatePreset
446
+
447
+ Save and restore named state presets to synced storage.
448
+
449
+ ```ts
450
+ interface StatePresetProps<TValue> {
451
+ storageKey: string;
452
+ value: TValue;
453
+ onValueChange: (value: TValue) => void;
454
+ size?: ComponentSize;
455
+ class?: string;
456
+ style?: JSX.CSSProperties;
457
+ }
458
+ ```
459
+
460
+ ## ThemeToggle
461
+
462
+ Theme cycle button (light -> system -> dark -> light). Must be used inside `ThemeProvider`.
463
+
464
+ ```ts
465
+ interface ThemeToggleProps extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
466
+ size?: ComponentSize;
467
+ }
468
+ ```
469
+
470
+ ## Invalid
471
+
472
+ Validation indicator wrapper. Wraps a child element and shows a validation error via dot or border.
473
+
474
+ ```ts
475
+ interface InvalidProps {
476
+ /** Validation error message. Non-empty = invalid. */
477
+ message?: string;
478
+ /** Visual indicator variant */
479
+ variant?: "border" | "dot";
480
+ /** When true, visual display only appears after target loses focus */
481
+ lazyValidation?: boolean;
482
+ }
483
+ ```
484
+
485
+ ## Field Style Exports
486
+
487
+ Exported from `src/components/form-control/field/Field.styles.ts`:
488
+
489
+ ```ts
490
+ const fieldSurface: string; // bg + text + border + focus-within
491
+ const fieldBaseClass: string; // inline-flex + fieldSurface
492
+ const fieldSizeClasses: Record<ComponentSize, string>;
493
+ const fieldInputClass: string; // input element styles
494
+ function getFieldWrapperClass(options: {
495
+ size?: ComponentSize;
496
+ disabled?: boolean;
497
+ inset?: boolean;
498
+ includeCustomClass?: string | false;
499
+ extra?: string | false;
500
+ }): string;
501
+ function getTextareaWrapperClass(options: {
502
+ size?: ComponentSize;
503
+ disabled?: boolean;
504
+ inset?: boolean;
505
+ includeCustomClass?: string | false;
506
+ }): string;
507
+ ```
508
+
509
+ ## Checkbox Style Exports
510
+
511
+ Exported from `src/components/form-control/checkbox/Checkbox.styles.ts`:
512
+
513
+ ```ts
514
+ type CheckboxSize = ComponentSize;
515
+ const checkboxBaseClass: string;
516
+ const indicatorBaseClass: string;
517
+ const checkedClass: string;
518
+ const checkboxSizeClasses: Record<CheckboxSize, string>;
519
+ ```
520
+
521
+ ## DropdownTrigger Style Exports
522
+
523
+ Exported from `src/components/form-control/DropdownTrigger.styles.ts`:
524
+
525
+ ```ts
526
+ const triggerBaseClass: string;
527
+ const triggerDisabledClass: string;
528
+ const triggerInsetClass: string;
529
+ const triggerSizeClasses: Record<ComponentSize, string>;
530
+ const chevronWrapperClass: string;
531
+ function getTriggerClass(options: {
532
+ size?: ComponentSize;
533
+ disabled?: boolean;
534
+ inset?: boolean;
535
+ class?: string;
536
+ }): string;
537
+ ```
@@ -0,0 +1,168 @@
1
+ # Helpers
2
+
3
+ Source: `src/helpers/*.ts`
4
+
5
+ ## mergeStyles
6
+
7
+ Utility function that merges CSS styles from multiple sources (objects and strings).
8
+
9
+ ```ts
10
+ function mergeStyles(
11
+ ...styles: (JSX.CSSProperties | string | undefined)[]
12
+ ): JSX.CSSProperties;
13
+ ```
14
+
15
+ - Object styles are merged with later values taking precedence.
16
+ - String styles are parsed (semicolon-delimited, kebab-case converted to camelCase).
17
+ - `undefined` values are ignored.
18
+
19
+ ## createAppStructure
20
+
21
+ Builds app menus, routes, permissions, and flat menu lists from a declarative structure definition. Supports module-based filtering and permission inference.
22
+
23
+ ```ts
24
+ function createAppStructure<TModule, const TItems extends AppStructureItem<TModule>[]>(
25
+ items: TItems,
26
+ options: {
27
+ basePath: string;
28
+ enabledModules?: TModule[];
29
+ enabledPerms?: string[];
30
+ homeCode?: string;
31
+ },
32
+ ): AppStructure<TModule>;
33
+ ```
34
+
35
+ ### Input Types
36
+
37
+ ```ts
38
+ interface AppStructureGroupItem<TModule> {
39
+ code: string;
40
+ title: string;
41
+ icon?: Component<IconProps>;
42
+ modules?: TModule[];
43
+ requiredModules?: TModule[];
44
+ children: AppStructureItem<TModule>[];
45
+ }
46
+
47
+ interface AppStructureLeafItem<TModule> {
48
+ code: string;
49
+ title: string;
50
+ icon?: Component<IconProps>;
51
+ modules?: TModule[];
52
+ requiredModules?: TModule[];
53
+ component?: Component;
54
+ perms?: ("use" | "edit")[];
55
+ subPerms?: AppStructureSubPerm<TModule>[];
56
+ isNotMenu?: boolean;
57
+ }
58
+
59
+ type AppStructureItem<TModule> =
60
+ | AppStructureGroupItem<TModule>
61
+ | AppStructureLeafItem<TModule>;
62
+
63
+ interface AppStructureSubPerm<TModule> {
64
+ code: string;
65
+ title: string;
66
+ modules?: TModule[];
67
+ requiredModules?: TModule[];
68
+ perms: ("use" | "edit")[];
69
+ }
70
+ ```
71
+
72
+ ### Output Types
73
+
74
+ ```ts
75
+ interface AppStructure<TModule> {
76
+ items: AppStructureItem<TModule>[];
77
+ usableRoutes: Accessor<AppRoute[]>;
78
+ usableMenus: Accessor<AppMenu[]>;
79
+ usableFlatMenus: Accessor<AppFlatMenu[]>;
80
+ usablePerms: Accessor<AppPerm<TModule>[]>;
81
+ allFlatPerms: AppFlatPerm<TModule>[];
82
+ getTitleChainByHref(href: string): string[];
83
+ }
84
+
85
+ interface AppMenu {
86
+ title: string;
87
+ href?: string;
88
+ icon?: Component<IconProps>;
89
+ children?: AppMenu[];
90
+ }
91
+
92
+ interface AppPerm<TModule = string> {
93
+ title: string;
94
+ href?: string;
95
+ modules?: TModule[];
96
+ perms?: string[];
97
+ children?: AppPerm<TModule>[];
98
+ }
99
+
100
+ interface AppRoute {
101
+ path: string;
102
+ component: Component;
103
+ }
104
+
105
+ interface AppFlatMenu {
106
+ titleChain: string[];
107
+ href: string;
108
+ }
109
+
110
+ interface AppFlatPerm<TModule = string> {
111
+ titleChain: string[];
112
+ code: string;
113
+ modulesChain: TModule[][];
114
+ requiredModulesChain: TModule[][];
115
+ }
116
+ ```
117
+
118
+ ## createSlot
119
+
120
+ Single-item slot pattern for parent-child communication. Used for patterns like `Dialog.Header`, `Select.Header`.
121
+
122
+ ```ts
123
+ function createSlot<TItem>(): [
124
+ SlotComponent: (props: TItem) => null,
125
+ createSlotAccessor: () => [Accessor<TItem | undefined>, ParentComponent],
126
+ ];
127
+ ```
128
+
129
+ Usage pattern:
130
+
131
+ ```tsx
132
+ // 1. Create slot at module level
133
+ const [HeaderSlot, createHeaderAccessor] = createSlot<{ children: JSX.Element }>();
134
+
135
+ // 2. In parent component
136
+ const [header, HeaderProvider] = createHeaderAccessor();
137
+
138
+ return (
139
+ <HeaderProvider>
140
+ {props.children}
141
+ <Show when={header()}>{header()!.children}</Show>
142
+ </HeaderProvider>
143
+ );
144
+
145
+ // 3. In child usage
146
+ <Parent>
147
+ <HeaderSlot>My Header Content</HeaderSlot>
148
+ {/* other content */}
149
+ </Parent>
150
+ ```
151
+
152
+ ## createSlots
153
+
154
+ Multi-item slot pattern. Like `createSlot` but collects an array of items.
155
+
156
+ ```ts
157
+ interface SlotRegistrar<TItem> {
158
+ add: (item: TItem) => void;
159
+ remove: (item: TItem) => void;
160
+ }
161
+
162
+ function createSlots<TItem>(): [
163
+ SlotComponent: (props: TItem) => null,
164
+ createSlotsAccessor: () => [Accessor<TItem[]>, ParentComponent],
165
+ ];
166
+ ```
167
+
168
+ Items are collected in order of registration and removed on cleanup.