@foldkit/ui 0.127.0 → 0.128.1

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.
Files changed (79) hide show
  1. package/README.md +2 -2
  2. package/dist/calendar/index.d.ts +17 -23
  3. package/dist/calendar/index.d.ts.map +1 -1
  4. package/dist/calendar/index.js +16 -25
  5. package/dist/calendar/public.d.ts +1 -1
  6. package/dist/calendar/public.d.ts.map +1 -1
  7. package/dist/calendar/public.js +1 -1
  8. package/dist/checkbox/index.d.ts +47 -104
  9. package/dist/checkbox/index.d.ts.map +1 -1
  10. package/dist/checkbox/index.js +48 -87
  11. package/dist/checkbox/public.d.ts +2 -2
  12. package/dist/checkbox/public.d.ts.map +1 -1
  13. package/dist/checkbox/public.js +1 -1
  14. package/dist/combobox/multi.d.ts +19 -30
  15. package/dist/combobox/multi.d.ts.map +1 -1
  16. package/dist/combobox/multi.js +32 -53
  17. package/dist/combobox/public.d.ts +2 -2
  18. package/dist/combobox/public.d.ts.map +1 -1
  19. package/dist/combobox/public.js +1 -1
  20. package/dist/combobox/shared.d.ts +59 -35
  21. package/dist/combobox/shared.d.ts.map +1 -1
  22. package/dist/combobox/shared.js +58 -45
  23. package/dist/combobox/single.d.ts +36 -48
  24. package/dist/combobox/single.d.ts.map +1 -1
  25. package/dist/combobox/single.js +49 -76
  26. package/dist/datePicker/index.d.ts +33 -41
  27. package/dist/datePicker/index.d.ts.map +1 -1
  28. package/dist/datePicker/index.js +31 -40
  29. package/dist/datePicker/public.d.ts +1 -1
  30. package/dist/datePicker/public.d.ts.map +1 -1
  31. package/dist/datePicker/public.js +1 -1
  32. package/dist/disclosure/index.d.ts +49 -113
  33. package/dist/disclosure/index.d.ts.map +1 -1
  34. package/dist/disclosure/index.js +35 -92
  35. package/dist/disclosure/public.d.ts +2 -2
  36. package/dist/disclosure/public.d.ts.map +1 -1
  37. package/dist/disclosure/public.js +1 -1
  38. package/dist/listbox/multi.d.ts +7 -21
  39. package/dist/listbox/multi.d.ts.map +1 -1
  40. package/dist/listbox/multi.js +12 -36
  41. package/dist/listbox/public.d.ts +1 -1
  42. package/dist/listbox/public.d.ts.map +1 -1
  43. package/dist/listbox/shared.d.ts +28 -20
  44. package/dist/listbox/shared.d.ts.map +1 -1
  45. package/dist/listbox/shared.js +6 -7
  46. package/dist/listbox/single.d.ts +17 -27
  47. package/dist/listbox/single.d.ts.map +1 -1
  48. package/dist/listbox/single.js +18 -30
  49. package/dist/radioGroup/index.d.ts +57 -142
  50. package/dist/radioGroup/index.d.ts.map +1 -1
  51. package/dist/radioGroup/index.js +44 -119
  52. package/dist/radioGroup/public.d.ts +2 -2
  53. package/dist/radioGroup/public.d.ts.map +1 -1
  54. package/dist/radioGroup/public.js +1 -1
  55. package/dist/slider/index.d.ts +51 -27
  56. package/dist/slider/index.d.ts.map +1 -1
  57. package/dist/slider/index.js +66 -66
  58. package/dist/slider/public.d.ts +1 -1
  59. package/dist/slider/public.d.ts.map +1 -1
  60. package/dist/slider/public.js +1 -1
  61. package/dist/switch/index.d.ts +45 -92
  62. package/dist/switch/index.d.ts.map +1 -1
  63. package/dist/switch/index.js +36 -81
  64. package/dist/switch/public.d.ts +2 -2
  65. package/dist/switch/public.d.ts.map +1 -1
  66. package/dist/switch/public.js +1 -1
  67. package/dist/tabs/index.d.ts +33 -33
  68. package/dist/tabs/index.d.ts.map +1 -1
  69. package/dist/tabs/index.js +36 -49
  70. package/dist/toast/index.d.ts +1 -0
  71. package/dist/toast/index.d.ts.map +1 -1
  72. package/dist/toast/index.js +1 -0
  73. package/dist/toast/public.d.ts +2 -0
  74. package/dist/toast/public.d.ts.map +1 -1
  75. package/dist/toast/public.js +1 -0
  76. package/dist/toast/test.d.ts +41 -0
  77. package/dist/toast/test.d.ts.map +1 -0
  78. package/dist/toast/test.js +36 -0
  79. package/package.json +5 -5
@@ -9,23 +9,30 @@ import * as Subscription from 'foldkit/subscription';
9
9
  const Idle = ts('Idle');
10
10
  const Dragging = ts('Dragging', { originValue: S.Number });
11
11
  const DragState = S.Union([Idle, Dragging]);
12
- /** Schema for the slider component's state. Tracks the current value, the
13
- * range (min/max/step), and the active drag phase. */
12
+ /** Schema for the slider component's private interaction state. The current
13
+ * value is owned by the parent and passed in via `ViewInputs.value`, so it is
14
+ * not stored here. `min`/`max`/`step` are configuration the drag subscription
15
+ * reads to map pointer positions into values. `dragState` tracks the active
16
+ * drag phase and captures the pre-drag value so Escape can restore it. */
14
17
  export const Model = S.Struct({
15
18
  id: S.String,
16
- value: S.Number,
17
19
  min: S.Number,
18
20
  max: S.Number,
19
21
  step: S.Number,
20
22
  dragState: DragState,
21
23
  });
22
24
  // MESSAGE
23
- /** The user pressed the thumb. Starts a drag without changing the value. */
24
- export const PressedThumb = m('PressedThumb');
25
+ /** The user pressed the thumb. Starts a drag without changing the value. The
26
+ * view supplies `originValue`, the current value, so Escape can restore it. */
27
+ export const PressedThumb = m('PressedThumb', { originValue: S.Number });
25
28
  /** The user pressed the track. Starts a drag and snaps the value to the
26
29
  * cursor position. Ignored while already dragging, which absorbs the bubble
27
- * from a thumb press so the value is not shifted. */
28
- export const PressedPointer = m('PressedPointer', { value: S.Number });
30
+ * from a thumb press so the value is not shifted. `originValue` is the current
31
+ * value the drag restores to on Escape. */
32
+ export const PressedPointer = m('PressedPointer', {
33
+ value: S.Number,
34
+ originValue: S.Number,
35
+ });
29
36
  /** The pointer moved during a drag, producing a new snapped value from the
30
37
  * cursor position within the track. */
31
38
  export const MovedDragPointer = m('MovedDragPointer', { value: S.Number });
@@ -33,7 +40,8 @@ export const MovedDragPointer = m('MovedDragPointer', { value: S.Number });
33
40
  export const ReleasedDragPointer = m('ReleasedDragPointer');
34
41
  /** Escape was pressed during a drag. Restores the value from the drag origin. */
35
42
  export const CancelledDrag = m('CancelledDrag');
36
- /** The user pressed a keyboard navigation key on the focused thumb. */
43
+ /** The user pressed a keyboard navigation key on the focused thumb. The view
44
+ * supplies `value`, the current value, to compute the next one from. */
37
45
  export const PressedKeyboardNavigation = m('PressedKeyboardNavigation', {
38
46
  direction: S.Literals([
39
47
  'StepDecrement',
@@ -43,6 +51,7 @@ export const PressedKeyboardNavigation = m('PressedKeyboardNavigation', {
43
51
  'Min',
44
52
  'Max',
45
53
  ]),
54
+ value: S.Number,
46
55
  });
47
56
  /** Union of all messages the slider component can produce. */
48
57
  export const Message = S.Union([
@@ -60,11 +69,10 @@ export const Message = S.Union([
60
69
  export const ChangedValue = m('ChangedValue', { value: S.Number });
61
70
  /** Union of all out-messages the slider component can emit to its parent. */
62
71
  export const OutMessage = S.Union([ChangedValue]);
63
- /** Creates an initial slider model from a config. The initial value is
64
- * snapped to the step and clamped into range. */
72
+ /** Creates an initial slider model from a config. The value lives in the
73
+ * parent Model; initialize it there and snap it with {@link snapAndClamp}. */
65
74
  export const init = (config) => ({
66
75
  id: config.id,
67
- value: snapAndClamp(config.initialValue, config.min, config.max, config.step),
68
76
  min: config.min,
69
77
  max: config.max,
70
78
  step: config.step,
@@ -83,43 +91,37 @@ const roundToStepPrecision = (value, step) => {
83
91
  return Number(value.toFixed(decimals));
84
92
  };
85
93
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
86
- const snapAndClamp = (value, min, max, step) => {
94
+ /** Snaps a value to the nearest step and clamps it into `[min, max]`. Exported
95
+ * so a parent can conform the value it owns to the slider's range, for example
96
+ * when seeding the initial value or reacting to an external update. */
97
+ export const snapAndClamp = (value, min, max, step) => {
87
98
  const snapped = min + Math.round((value - min) / step) * step;
88
99
  return roundToStepPrecision(clamp(snapped, min, max), step);
89
100
  };
90
101
  /** Computes the fraction (0–1) of a value between min and max. Returns 0 when
91
102
  * the range has zero width. */
92
- export const fractionOfValue = (model) => {
93
- const range = model.max - model.min;
103
+ export const fractionOfValue = (value, min, max) => {
104
+ const range = max - min;
94
105
  if (range <= 0) {
95
106
  return 0;
96
107
  }
97
108
  else {
98
- return clamp((model.value - model.min) / range, 0, 1);
109
+ return clamp((value - min) / range, 0, 1);
99
110
  }
100
111
  };
101
112
  const PAGE_STEP_MULTIPLIER = 10;
102
- const nextValueForDirection = (model, direction) => M.value(direction).pipe(M.withReturnType(), M.when('StepIncrement', () => snapAndClamp(model.value + model.step, model.min, model.max, model.step)), M.when('StepDecrement', () => snapAndClamp(model.value - model.step, model.min, model.max, model.step)), M.when('PageIncrement', () => snapAndClamp(model.value + model.step * PAGE_STEP_MULTIPLIER, model.min, model.max, model.step)), M.when('PageDecrement', () => snapAndClamp(model.value - model.step * PAGE_STEP_MULTIPLIER, model.min, model.max, model.step)), M.when('Min', () => model.min), M.when('Max', () => model.max), M.exhaustive);
113
+ const nextValueForDirection = (value, min, max, step, direction) => M.value(direction).pipe(M.withReturnType(), M.when('StepIncrement', () => snapAndClamp(value + step, min, max, step)), M.when('StepDecrement', () => snapAndClamp(value - step, min, max, step)), M.when('PageIncrement', () => snapAndClamp(value + step * PAGE_STEP_MULTIPLIER, min, max, step)), M.when('PageDecrement', () => snapAndClamp(value - step * PAGE_STEP_MULTIPLIER, min, max, step)), M.when('Min', () => min), M.when('Max', () => max), M.exhaustive);
103
114
  const withUpdateReturn = M.withReturnType();
104
- const withValue = (model, nextValue, commands) => {
105
- if (nextValue === model.value) {
106
- return [model, commands, Option.none()];
107
- }
108
- else {
109
- return [
110
- evo(model, { value: () => nextValue }),
111
- commands,
112
- Option.some(ChangedValue({ value: nextValue })),
113
- ];
114
- }
115
- };
115
+ const changedValueOption = (currentValue, nextValue) => nextValue === currentValue
116
+ ? Option.none()
117
+ : Option.some(ChangedValue({ value: nextValue }));
116
118
  /** Processes a slider message and returns the next model, commands, and an
117
- * optional out-message for the parent. */
119
+ * optional out-message for the parent. The value lives in the parent Model:
120
+ * the view supplies the current value on the messages that need it, and value
121
+ * changes surface as `ChangedValue` rather than mutating this Model. */
118
122
  export const update = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
119
- PressedThumb: () => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => [model, [], Option.none()]), M.orElse(() => [
120
- evo(model, {
121
- dragState: () => Dragging({ originValue: model.value }),
122
- }),
123
+ PressedThumb: ({ originValue }) => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => [model, [], Option.none()]), M.orElse(() => [
124
+ evo(model, { dragState: () => Dragging({ originValue }) }),
123
125
  [],
124
126
  Option.none(),
125
127
  ])),
@@ -129,48 +131,45 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
129
131
  // from the thumb's current position. Fine-grained sliders (e.g. step
130
132
  // 0.05) see a visible jump without this guard, because the cursor sits
131
133
  // off-center on a non-zero-width thumb.
132
- PressedPointer: ({ value }) => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => [model, [], Option.none()]), M.orElse(() => {
134
+ PressedPointer: ({ value, originValue }) => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => [model, [], Option.none()]), M.orElse(() => {
133
135
  const snapped = snapAndClamp(value, model.min, model.max, model.step);
134
- const [modelWithValue, commands, maybeOutMessage] = withValue(model, snapped, []);
135
136
  return [
136
- evo(modelWithValue, {
137
- dragState: () => Dragging({ originValue: model.value }),
138
- }),
139
- commands,
140
- maybeOutMessage,
137
+ evo(model, { dragState: () => Dragging({ originValue }) }),
138
+ [],
139
+ changedValueOption(originValue, snapped),
141
140
  ];
142
141
  })),
143
- MovedDragPointer: ({ value }) => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => withValue(model, snapAndClamp(value, model.min, model.max, model.step), [])), M.orElse(() => [model, [], Option.none()])),
142
+ MovedDragPointer: ({ value }) => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => [
143
+ model,
144
+ [],
145
+ Option.some(ChangedValue({
146
+ value: snapAndClamp(value, model.min, model.max, model.step),
147
+ })),
148
+ ]), M.orElse(() => [model, [], Option.none()])),
144
149
  ReleasedDragPointer: () => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', () => [
145
150
  evo(model, { dragState: () => Idle() }),
146
151
  [],
147
152
  Option.none(),
148
153
  ]), M.orElse(() => [model, [], Option.none()])),
149
- CancelledDrag: () => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', ({ originValue }) => {
150
- const restored = evo(model, {
151
- dragState: () => Idle(),
152
- });
153
- return withValue(restored, originValue, []);
154
- }), M.orElse(() => [model, [], Option.none()])),
155
- PressedKeyboardNavigation: ({ direction }) => withValue(model, nextValueForDirection(model, direction), []),
154
+ CancelledDrag: () => M.value(model.dragState).pipe(withUpdateReturn, M.tag('Dragging', ({ originValue }) => [
155
+ evo(model, { dragState: () => Idle() }),
156
+ [],
157
+ Option.some(ChangedValue({ value: originValue })),
158
+ ]), M.orElse(() => [model, [], Option.none()])),
159
+ PressedKeyboardNavigation: ({ direction, value }) => [
160
+ model,
161
+ [],
162
+ changedValueOption(value, nextValueForDirection(value, model.min, model.max, model.step, direction)),
163
+ ],
156
164
  }));
157
- /** Reflects an externally-driven range onto the slider. Snaps and clamps the
158
- * current value into the new range. Use this when min/max derive from
159
- * external state (e.g. a bounded buffer whose first/last index shifts over
160
- * time). Unlike `reflectValue`, this runs even while the user is Dragging: a
161
- * structural range change cannot leave the value out of bounds. */
165
+ /** Reflects an externally-driven range onto the slider. Use this when min/max
166
+ * derive from external state (e.g. a bounded buffer whose first/last index
167
+ * shifts over time). The parent owns the value, so conform it to the new range
168
+ * in the same update with {@link snapAndClamp}. */
162
169
  export const reflectRange = Function.dual(2, (model, range) => evo(model, {
163
170
  min: () => range.min,
164
171
  max: () => range.max,
165
- value: current => snapAndClamp(current, range.min, range.max, model.step),
166
172
  }));
167
- /** Reflects an externally-driven value onto the slider, snapped and clamped
168
- * into range; a no-op while the user is dragging, since drag state owns the
169
- * value. Does not emit `ChangedValue`; use when the value is being driven by
170
- * external state rather than user input. */
171
- export const reflectValue = Function.dual(2, (model, value) => M.value(model.dragState).pipe(M.withReturnType(), M.tag('Dragging', () => model), M.orElse(() => evo(model, {
172
- value: () => snapAndClamp(value, model.min, model.max, model.step),
173
- }))));
174
173
  // SUBSCRIPTION
175
174
  const DragActivity = S.Literals(['Idle', 'Active']);
176
175
  const dragActivityFromModel = (model) => M.value(model.dragState).pipe(M.withReturnType(), M.tag('Dragging', () => 'Active'), M.orElse(() => 'Idle'));
@@ -244,16 +243,17 @@ const percentString = (fraction) => `${Math.round(fraction * 10000) / 100}%`;
244
243
  * end. Pointer drag is handled by the component's drag subscriptions. */
245
244
  export const view = defineView((model, viewInputs) => {
246
245
  const h = html();
247
- const { formatValue, isDisabled = false, name, getTrackRoot = () => document, } = viewInputs;
248
- const { id, value, min, max } = model;
246
+ const { value, formatValue, isDisabled = false, name, getTrackRoot = () => document, } = viewInputs;
247
+ const { id, min, max } = model;
249
248
  const isDragging = model.dragState._tag === 'Dragging';
250
- const fraction = fractionOfValue(model);
251
- const handleKeyDown = (key) => Option.map(keyToDirection(key), direction => PressedKeyboardNavigation({ direction }));
249
+ const fraction = fractionOfValue(value, min, max);
250
+ const handleKeyDown = (key) => Option.map(keyToDirection(key), direction => PressedKeyboardNavigation({ direction, value }));
252
251
  const pointerAtClientX = (clientX) => Option.map(trackElement(id, getTrackRoot()), element => PressedPointer({
253
252
  value: valueFromClientX(clientX, element, min, max),
253
+ originValue: value,
254
254
  }));
255
255
  const trackPointerHandler = (_pointerType, button, _screenX, _screenY, _timeStamp, clientX) => pipe(button, Option.liftPredicate(Equal.equals(LEFT_MOUSE_BUTTON)), Option.flatMap(() => pointerAtClientX(clientX)));
256
- const thumbPointerHandler = (_pointerType, button) => pipe(button, Option.liftPredicate(Equal.equals(LEFT_MOUSE_BUTTON)), Option.map(() => PressedThumb()));
256
+ const thumbPointerHandler = (_pointerType, button) => pipe(button, Option.liftPredicate(Equal.equals(LEFT_MOUSE_BUTTON)), Option.map(() => PressedThumb({ originValue: value })));
257
257
  const stateAttributes = [
258
258
  ...(isDragging ? [h.DataAttribute('dragging', '')] : []),
259
259
  ...(isDisabled ? [h.DataAttribute('disabled', '')] : []),
@@ -1,3 +1,3 @@
1
- export { init, update, reflectRange, reflectValue, view, subscriptions, subscriptionsForRoot, fractionOfValue, Model, Message, OutMessage, } from './index.js';
1
+ export { init, update, reflectRange, view, subscriptions, subscriptionsForRoot, fractionOfValue, snapAndClamp, Model, Message, OutMessage, } from './index.js';
2
2
  export type { InitConfig, ViewInputs, SliderAttributes, PressedThumb, PressedPointer, MovedDragPointer, ReleasedDragPointer, CancelledDrag, PressedKeyboardNavigation, } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/slider/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,IAAI,EACJ,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,KAAK,EACL,OAAO,EACP,UAAU,GACX,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,yBAAyB,GAC1B,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/slider/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,IAAI,EACJ,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,KAAK,EACL,OAAO,EACP,UAAU,GACX,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,yBAAyB,GAC1B,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { init, update, reflectRange, reflectValue, view, subscriptions, subscriptionsForRoot, fractionOfValue, Model, Message, OutMessage, } from './index.js';
1
+ export { init, update, reflectRange, view, subscriptions, subscriptionsForRoot, fractionOfValue, snapAndClamp, Model, Message, OutMessage, } from './index.js';
@@ -1,99 +1,52 @@
1
- import { Option, Schema as S } from 'effect';
2
- import type { Command } from 'foldkit/command';
3
- import { type ChildAttribute, type Html } from 'foldkit/html';
4
- import { type Reflect } from 'foldkit/submodel';
5
- /** Schema for the switch component's state, tracking the toggle's checked status. */
6
- export declare const Model: S.Struct<{
7
- readonly id: S.String;
8
- readonly isChecked: S.Boolean;
1
+ import { type Attribute, type Html } from 'foldkit/html';
2
+ /** Attribute groups the switch provides to the consumer's `toView` callback.
3
+ * Each group is a `ReadonlyArray<Attribute<Message>>` the consumer
4
+ * spreads into its own element attribute arrays. The `button` and `label`
5
+ * bundles carry the click and Space handlers that dispatch the configured
6
+ * `onToggle` Message. */
7
+ export type SwitchAttributes<Message> = Readonly<{
8
+ button: ReadonlyArray<Attribute<Message>>;
9
+ label: ReadonlyArray<Attribute<Message>>;
10
+ description: ReadonlyArray<Attribute<Message>>;
11
+ hiddenInput: ReadonlyArray<Attribute<Message>>;
9
12
  }>;
10
- export type Model = typeof Model.Type;
11
- /** Sent when the user toggles the switch via click or Space key. */
12
- export declare const Toggled: import("foldkit/schema").CallableTaggedStruct<"Toggled", {}>;
13
- /** Sent to set the checked state to a specific value. Use this for
14
- * programmatic state assignment (e.g. a "select all" handler that forces
15
- * all child switches to the same state) where `Toggled`'s flip semantics
16
- * would not reliably reach the desired state. */
17
- export declare const SetChecked: import("foldkit/schema").CallableTaggedStruct<"SetChecked", {
18
- isChecked: S.Boolean;
19
- }>;
20
- /** Schema for all messages the switch component can produce. */
21
- export declare const Message: S.Union<readonly [import("foldkit/schema").CallableTaggedStruct<"Toggled", {}>, import("foldkit/schema").CallableTaggedStruct<"SetChecked", {
22
- isChecked: S.Boolean;
23
- }>]>;
24
- export type Toggled = typeof Toggled.Type;
25
- export type SetChecked = typeof SetChecked.Type;
26
- export type Message = typeof Message.Type;
27
- /** Sent to the parent each time the switch toggles. Carries the new
28
- * checked state. Consumers pattern-match this in their `GotSwitchMessage`
29
- * handler to lift the toggle into a domain Message (e.g., persisting the
30
- * setting, dispatching a sync command). */
31
- export declare const ToggledChecked: import("foldkit/schema").CallableTaggedStruct<"ToggledChecked", {
32
- isChecked: S.Boolean;
33
- }>;
34
- /** Union of out-messages the switch component can produce. Surfaced as
35
- * the third element of `update`'s return tuple and pattern-matched by
36
- * the parent. */
37
- export declare const OutMessage: S.Union<readonly [import("foldkit/schema").CallableTaggedStruct<"ToggledChecked", {
38
- isChecked: S.Boolean;
39
- }>]>;
40
- export type ToggledChecked = typeof ToggledChecked.Type;
41
- export type OutMessage = typeof OutMessage.Type;
42
- /** Configuration for creating a switch model with `init`. */
43
- export type InitConfig = Readonly<{
13
+ /** Per-render view configuration for the stateless controlled {@link view}.
14
+ * Generic over `Message` (the message `onToggle` dispatches).
15
+ *
16
+ * - `isChecked`: the current checked state, read straight from the parent
17
+ * Model. `aria-checked` and the `data-checked` marker derive from it.
18
+ * - `onToggle`: dispatched with the new checked state when the user clicks
19
+ * the switch or its label, or presses Space. Handle it in the parent's
20
+ * `update` by storing the value.
21
+ * - `toView`: receives the {@link SwitchAttributes} and lays out the
22
+ * switch. */
23
+ export type ViewConfig<Message> = Readonly<{
44
24
  id: string;
45
- isChecked?: boolean;
46
- }>;
47
- /** Creates an initial switch model from a config. Defaults to unchecked. */
48
- export declare const init: (config: InitConfig) => Model;
49
- /** Processes a switch message and returns the next model, commands, and
50
- * a `ToggledChecked` OutMessage carrying the new checked state. */
51
- export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<Command<Message>>, Option.Option<OutMessage>];
52
- /** Programmatically sets the checked state. Emits a `ToggledChecked`
53
- * OutMessage just like a user-initiated toggle. Use this in domain-event
54
- * handlers where you need to force a specific state. */
55
- export declare const setChecked: (model: Model, isChecked: boolean) => readonly [Model, ReadonlyArray<Command<Message>>, Option.Option<OutMessage>];
56
- /** Reflects an externally-sourced checked state onto the model without
57
- * emitting an OutMessage. Use this to mirror external truth (saved
58
- * settings, a server value) onto the switch without triggering the
59
- * downstream reaction a user toggle would cause. Contrast with
60
- * `setChecked`, which emits `ToggledChecked` so the parent reacts to a
61
- * programmatic assignment the same way it reacts to a user toggle. Returns
62
- * the model directly because it produces no commands and no OutMessage. */
63
- export declare const reflectChecked: Reflect<Model, boolean>;
64
- /** Attribute groups the switch component provides to the consumer's
65
- * `toView` callback. Each group is a `ReadonlyArray<ChildAttribute>`
66
- * whose event handlers dispatch through the Switch's boundary at
67
- * event-fire time. See {@link Checkbox.CheckboxAttributes} for the full
68
- * routing model. */
69
- export type SwitchAttributes = Readonly<{
70
- button: ReadonlyArray<ChildAttribute>;
71
- label: ReadonlyArray<ChildAttribute>;
72
- description: ReadonlyArray<ChildAttribute>;
73
- hiddenInput: ReadonlyArray<ChildAttribute>;
74
- }>;
75
- /** Per-render view inputs passed to `view` via `h.submodel`'s `viewInputs` field. */
76
- export type ViewInputs = Readonly<{
77
- toView: (attributes: SwitchAttributes) => Html;
25
+ isChecked: boolean;
26
+ onToggle: (isChecked: boolean) => Message;
27
+ toView: (attributes: SwitchAttributes<Message>) => Html;
78
28
  isDisabled?: boolean;
79
29
  name?: string;
80
30
  value?: string;
81
31
  }>;
82
- /** Renders an accessible switch toggle by building ARIA attribute groups
83
- * and delegating layout to the consumer's `toView` callback. Designed
84
- * to be embedded via `h.submodel`. */
85
- export declare const view: import("foldkit/submodel").View<S.Struct.ReadonlySide<{
86
- readonly id: S.String;
87
- readonly isChecked: S.Boolean;
88
- }, "Type">, S.Struct.ReadonlySide<{
89
- readonly _tag: S.tag<"Toggled">;
90
- }, "Type"> | S.Struct.ReadonlySide<{
91
- readonly _tag: S.tag<"SetChecked">;
92
- isChecked: S.Boolean;
93
- }, "Type">, Readonly<{
94
- toView: (attributes: SwitchAttributes) => Html;
95
- isDisabled?: boolean;
96
- name?: string;
97
- value?: string;
98
- }>>;
32
+ /** Renders an accessible switch as a stateless controlled component. The
33
+ * parent owns the checked state (`isChecked`) and receives the new state via
34
+ * `onToggle` when the user toggles it.
35
+ *
36
+ * ```ts
37
+ * // In view:
38
+ * Switch.view<Message>({
39
+ * id: 'notifications',
40
+ * isChecked: model.notificationsEnabled,
41
+ * onToggle: isChecked => ToggledNotifications({ isChecked }),
42
+ * toView: attributes => ...,
43
+ * })
44
+ *
45
+ * // In update:
46
+ * ToggledNotifications: ({ isChecked }) => [
47
+ * evo(model, { notificationsEnabled: () => isChecked }),
48
+ * [],
49
+ * ],
50
+ * ``` */
51
+ export declare const view: <Message>(config: ViewConfig<Message>) => Html;
99
52
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/switch/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,IAAI,EAGV,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,KAAK,OAAO,EAAc,MAAM,kBAAkB,CAAA;AAI3D,qFAAqF;AACrF,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,oEAAoE;AACpE,eAAO,MAAM,OAAO,8DAAe,CAAA;AAEnC;;;kDAGkD;AAClD,eAAO,MAAM,UAAU;;EAA4C,CAAA;AAEnE,gEAAgE;AAChE,eAAO,MAAM,OAAO;;IAAiC,CAAA;AAErD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAE/C,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC;;;4CAG4C;AAC5C,eAAO,MAAM,cAAc;;EAAgD,CAAA;AAE3E;;kBAEkB;AAClB,eAAO,MAAM,UAAU;;IAA4B,CAAA;AAEnD,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AACvD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAI/C,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAGxC,CAAA;AAIF;oEACoE;AACpE,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CACV,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAC/B,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAyBxB,CAAA;AAEH;;yDAEyD;AACzD,eAAO,MAAM,UAAU,GACrB,OAAO,KAAK,EACZ,WAAW,OAAO,KACjB,SAAS,CACV,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAC/B,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CACkB,CAAA;AAE7C;;;;;;4EAM4E;AAC5E,eAAO,MAAM,cAAc,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAIlD,CAAA;AAID;;;;qBAIqB;AACrB,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,MAAM,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;IACrC,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;IACpC,WAAW,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;IAC1C,WAAW,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;CAC3C,CAAC,CAAA;AAEF,qFAAqF;AACrF,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,MAAM,EAAE,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC9C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CAAA;AAKF;;uCAEuC;AACvC,eAAO,MAAM,IAAI;;;;;;;;;YAZP,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI;iBACjC,OAAO;WACb,MAAM;YACL,MAAM;GA2Df,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/switch/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,IAAI,EAAQ,MAAM,cAAc,CAAA;AAI9D;;;;0BAI0B;AAC1B,MAAM,MAAM,gBAAgB,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC/C,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IACzC,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IACxC,WAAW,EAAE,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IAC9C,WAAW,EAAE,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;CAC/C,CAAC,CAAA;AAEF;;;;;;;;;gBASgB;AAChB,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAA;IACzC,MAAM,EAAE,CAAC,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IACvD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CAAA;AAKF;;;;;;;;;;;;;;;;;;UAkBU;AACV,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,QAAQ,UAAU,CAAC,OAAO,CAAC,KAAG,IA4D3D,CAAA"}
@@ -1,79 +1,31 @@
1
- import { Function, Match as M, Option, Schema as S } from 'effect';
2
- import { childAttributes, html, } from 'foldkit/html';
3
- import { m } from 'foldkit/message';
4
- import { evo } from 'foldkit/struct';
5
- import { defineView } from 'foldkit/submodel';
6
- // MODEL
7
- /** Schema for the switch component's state, tracking the toggle's checked status. */
8
- export const Model = S.Struct({
9
- id: S.String,
10
- isChecked: S.Boolean,
11
- });
12
- // MESSAGE
13
- /** Sent when the user toggles the switch via click or Space key. */
14
- export const Toggled = m('Toggled');
15
- /** Sent to set the checked state to a specific value. Use this for
16
- * programmatic state assignment (e.g. a "select all" handler that forces
17
- * all child switches to the same state) where `Toggled`'s flip semantics
18
- * would not reliably reach the desired state. */
19
- export const SetChecked = m('SetChecked', { isChecked: S.Boolean });
20
- /** Schema for all messages the switch component can produce. */
21
- export const Message = S.Union([Toggled, SetChecked]);
22
- // OUT MESSAGE
23
- /** Sent to the parent each time the switch toggles. Carries the new
24
- * checked state. Consumers pattern-match this in their `GotSwitchMessage`
25
- * handler to lift the toggle into a domain Message (e.g., persisting the
26
- * setting, dispatching a sync command). */
27
- export const ToggledChecked = m('ToggledChecked', { isChecked: S.Boolean });
28
- /** Union of out-messages the switch component can produce. Surfaced as
29
- * the third element of `update`'s return tuple and pattern-matched by
30
- * the parent. */
31
- export const OutMessage = S.Union([ToggledChecked]);
32
- /** Creates an initial switch model from a config. Defaults to unchecked. */
33
- export const init = (config) => ({
34
- id: config.id,
35
- isChecked: config.isChecked ?? false,
36
- });
37
- // UPDATE
38
- /** Processes a switch message and returns the next model, commands, and
39
- * a `ToggledChecked` OutMessage carrying the new checked state. */
40
- export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
41
- Toggled: () => {
42
- const nextIsChecked = !model.isChecked;
43
- return [
44
- evo(model, { isChecked: () => nextIsChecked }),
45
- [],
46
- Option.some(ToggledChecked({ isChecked: nextIsChecked })),
47
- ];
48
- },
49
- SetChecked: ({ isChecked }) => [
50
- evo(model, { isChecked: () => isChecked }),
51
- [],
52
- Option.some(ToggledChecked({ isChecked })),
53
- ],
54
- }));
55
- /** Programmatically sets the checked state. Emits a `ToggledChecked`
56
- * OutMessage just like a user-initiated toggle. Use this in domain-event
57
- * handlers where you need to force a specific state. */
58
- export const setChecked = (model, isChecked) => update(model, SetChecked({ isChecked }));
59
- /** Reflects an externally-sourced checked state onto the model without
60
- * emitting an OutMessage. Use this to mirror external truth (saved
61
- * settings, a server value) onto the switch without triggering the
62
- * downstream reaction a user toggle would cause. Contrast with
63
- * `setChecked`, which emits `ToggledChecked` so the parent reacts to a
64
- * programmatic assignment the same way it reacts to a user toggle. Returns
65
- * the model directly because it produces no commands and no OutMessage. */
66
- export const reflectChecked = Function.dual(2, (model, isChecked) => evo(model, { isChecked: () => isChecked }));
1
+ import { Match as M, Option } from 'effect';
2
+ import { html } from 'foldkit/html';
67
3
  const labelId = (id) => `${id}-label`;
68
4
  const descriptionId = (id) => `${id}-description`;
69
- /** Renders an accessible switch toggle by building ARIA attribute groups
70
- * and delegating layout to the consumer's `toView` callback. Designed
71
- * to be embedded via `h.submodel`. */
72
- export const view = defineView((model, viewInputs) => {
5
+ /** Renders an accessible switch as a stateless controlled component. The
6
+ * parent owns the checked state (`isChecked`) and receives the new state via
7
+ * `onToggle` when the user toggles it.
8
+ *
9
+ * ```ts
10
+ * // In view:
11
+ * Switch.view<Message>({
12
+ * id: 'notifications',
13
+ * isChecked: model.notificationsEnabled,
14
+ * onToggle: isChecked => ToggledNotifications({ isChecked }),
15
+ * toView: attributes => ...,
16
+ * })
17
+ *
18
+ * // In update:
19
+ * ToggledNotifications: ({ isChecked }) => [
20
+ * evo(model, { notificationsEnabled: () => isChecked }),
21
+ * [],
22
+ * ],
23
+ * ``` */
24
+ export const view = (config) => {
73
25
  const h = html();
74
- const { id, isChecked } = model;
75
- const { isDisabled = false, name, value: formValue = 'on' } = viewInputs;
76
- const handleKeyUp = (key) => M.value(key).pipe(M.when(' ', () => Option.some(Toggled())), M.orElse(() => Option.none()));
26
+ const { id, isChecked, onToggle, toView, isDisabled = false, name, value: formValue = 'on', } = config;
27
+ const nextChecked = !isChecked;
28
+ const handleKeyUp = (key) => M.value(key).pipe(M.when(' ', () => Option.some(onToggle(nextChecked))), M.orElse(() => Option.none()));
77
29
  const checkedAttributes = isChecked ? [h.DataAttribute('checked', '')] : [];
78
30
  const disabledAttributes = isDisabled
79
31
  ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
@@ -88,20 +40,23 @@ export const view = defineView((model, viewInputs) => {
88
40
  ...disabledAttributes,
89
41
  ...(isDisabled
90
42
  ? []
91
- : [h.OnClick(Toggled()), h.OnKeyUpPreventDefault(handleKeyUp)]),
43
+ : [
44
+ h.OnClick(onToggle(nextChecked)),
45
+ h.OnKeyUpPreventDefault(handleKeyUp),
46
+ ]),
92
47
  ];
93
48
  const labelAttributes = [
94
49
  h.Id(labelId(id)),
95
- ...(isDisabled ? [] : [h.OnClick(Toggled())]),
50
+ ...(isDisabled ? [] : [h.OnClick(onToggle(nextChecked))]),
96
51
  ];
97
52
  const descriptionAttributes = [h.Id(descriptionId(id))];
98
53
  const hiddenInputAttributes = name
99
54
  ? [h.Type('hidden'), h.Name(name), h.Value(isChecked ? formValue : '')]
100
55
  : [];
101
- return viewInputs.toView({
102
- button: childAttributes(buttonAttributes),
103
- label: childAttributes(labelAttributes),
104
- description: childAttributes(descriptionAttributes),
105
- hiddenInput: childAttributes(hiddenInputAttributes),
56
+ return toView({
57
+ button: buttonAttributes,
58
+ label: labelAttributes,
59
+ description: descriptionAttributes,
60
+ hiddenInput: hiddenInputAttributes,
106
61
  });
107
- });
62
+ };
@@ -1,3 +1,3 @@
1
- export { init, update, setChecked, reflectChecked, view, Model, Message, OutMessage, SetChecked, ToggledChecked, } from './index.js';
2
- export type { Toggled, InitConfig, ViewInputs, SwitchAttributes, } from './index.js';
1
+ export { view } from './index.js';
2
+ export type { ViewConfig, SwitchAttributes } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/switch/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,UAAU,EACV,cAAc,EACd,IAAI,EACJ,KAAK,EACL,OAAO,EACP,UAAU,EACV,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,OAAO,EACP,UAAU,EACV,UAAU,EACV,gBAAgB,GACjB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/switch/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEjC,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { init, update, setChecked, reflectChecked, view, Model, Message, OutMessage, SetChecked, ToggledChecked, } from './index.js';
1
+ export { view } from './index.js';