@wcstack/state 1.26.0 → 1.27.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.
@@ -71,6 +71,15 @@ function valueMustBeBoolean(fnName) {
71
71
  function valueMustBeDate(fnName) {
72
72
  raiseError(`filter ${fnName} requires a date value`);
73
73
  }
74
+ /**
75
+ * Throws error when filter requires array value but non-array provided.
76
+ *
77
+ * @param fnName - Name of the filter function
78
+ * @returns Never returns (always throws)
79
+ */
80
+ function valueMustBeArray(fnName) {
81
+ raiseError(`filter ${fnName} requires an array value`);
82
+ }
74
83
 
75
84
  /**
76
85
  * builtinFilters.ts
@@ -83,7 +92,7 @@ function valueMustBeDate(fnName) {
83
92
  * - Designed for common use as both input and output filters
84
93
  *
85
94
  * Design points:
86
- * - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, fix, locale, uc, lc, cap, trim, slice, pad, int, float, round, date, time, ymd, falsy, truthy, defaults, boolean, number, string, null, etc.
95
+ * - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, abs, clamp, fix, locale, uc, lc, cap, trim, slice, pad, truncate, join, int, float, round, percent, unit, date, time, ymd, hms, falsy, truthy, defaults, boolean, number, string, null, etc.
87
96
  * - Rich type checking and error handling for option values
88
97
  * - Centralized management of filter functions with FilterWithOptions type, easy to extend
89
98
  * - Dynamic retrieval of filter functions from filter names and options via builtinFilterFn
@@ -316,6 +325,48 @@ const mod = (options) => {
316
325
  return value % Number(opt);
317
326
  };
318
327
  };
328
+ /**
329
+ * Absolute value filter - returns the magnitude of a number.
330
+ *
331
+ * @param options - Unused
332
+ * @returns Filter function that returns the absolute value
333
+ */
334
+ const abs = (_options) => {
335
+ return (value) => {
336
+ if (typeof value !== 'number') {
337
+ valueMustBeNumber('abs');
338
+ }
339
+ return Math.abs(value);
340
+ };
341
+ };
342
+ /**
343
+ * Clamp filter - constrains a number to the inclusive range [min, max].
344
+ *
345
+ * Saturating conversion in the same family as round/floor/ceil, so it stays on
346
+ * the wire rather than in state. Pairs with `unit` for style bindings:
347
+ * `style.width: ratio|clamp(0,1)|percent(0)`.
348
+ *
349
+ * @param options - Array with minimum as first element and maximum as second (both required)
350
+ * @returns Filter function that returns the clamped number
351
+ */
352
+ const clamp = (options) => {
353
+ const opt1 = options?.[0] ?? optionsRequired('clamp');
354
+ if (!validateNumberString(opt1)) {
355
+ optionMustBeNumber('clamp');
356
+ }
357
+ const opt2 = options?.[1] ?? optionsRequired('clamp');
358
+ if (!validateNumberString(opt2)) {
359
+ optionMustBeNumber('clamp');
360
+ }
361
+ const min = Number(opt1);
362
+ const max = Number(opt2);
363
+ return (value) => {
364
+ if (typeof value !== 'number') {
365
+ valueMustBeNumber('clamp');
366
+ }
367
+ return Math.min(Math.max(value, min), max);
368
+ };
369
+ };
319
370
  /**
320
371
  * Fixed decimal filter - formats number to fixed decimal places.
321
372
  *
@@ -582,6 +633,76 @@ const percent = (options) => {
582
633
  return `${(value * 100).toFixed(Number(opt))}%`;
583
634
  };
584
635
  };
636
+ /**
637
+ * Unit filter - appends a CSS unit (or any suffix) to the value.
638
+ *
639
+ * A number alone does nothing in CSS, so without this the unit has to be built in
640
+ * state — which drags presentation into the source of truth, and in the worst case
641
+ * forces a whole derived array just to carry `"42%"` strings.
642
+ * `style.height: samples.*.cpu|clamp(0,100)|fix(0)|unit(%)` keeps it on the wire.
643
+ *
644
+ * Accepts strings as well as numbers **on purpose**: the useful chains run through
645
+ * `fix` / `percent`, which already return strings. Rejecting non-numbers here would
646
+ * break exactly the combination this filter exists for.
647
+ *
648
+ * `null` / `undefined` pass through untouched rather than becoming `"undefinedpx"`,
649
+ * so the binding layer's "undefined skips the write, null clears" semantics survive.
650
+ *
651
+ * @param options - Array with the unit/suffix as first element (required)
652
+ * @returns Filter function that returns the value with the unit appended
653
+ */
654
+ const unit = (options) => {
655
+ const opt = options?.[0] ?? optionsRequired('unit');
656
+ return (value) => {
657
+ if (value === null || typeof value === 'undefined') {
658
+ return value;
659
+ }
660
+ return String(value) + opt;
661
+ };
662
+ };
663
+ /**
664
+ * Join filter - joins array elements into a string.
665
+ *
666
+ * The default separator is `", "` rather than `","`: a bare comma is what `String()`
667
+ * already produces without any filter, so defaulting to it would make `|join` a no-op.
668
+ *
669
+ * @param options - Array with separator as first element (default: ', ')
670
+ * @returns Filter function that returns the joined string
671
+ */
672
+ const join = (options) => {
673
+ const opt = options?.[0] ?? ', ';
674
+ return (value) => {
675
+ if (!Array.isArray(value)) {
676
+ valueMustBeArray('join');
677
+ }
678
+ return value.join(opt);
679
+ };
680
+ };
681
+ /**
682
+ * Truncate filter - shortens a string and appends an ellipsis.
683
+ *
684
+ * The length option counts **kept characters**, not the total including the suffix,
685
+ * matching the existing `slice(0, n)` reading. A string at or below the limit is
686
+ * returned untouched (no suffix).
687
+ *
688
+ * @param options - Array with max kept length as first element and suffix as second (default: '…')
689
+ * @returns Filter function that returns the truncated string
690
+ */
691
+ const truncate = (options) => {
692
+ const opt1 = options?.[0] ?? optionsRequired('truncate');
693
+ if (!validateNumberString(opt1)) {
694
+ optionMustBeNumber('truncate');
695
+ }
696
+ const maxLength = Number(opt1);
697
+ const suffix = options?.[1] ?? '…';
698
+ return (value) => {
699
+ const v = String(value);
700
+ if (v.length <= maxLength) {
701
+ return v;
702
+ }
703
+ return v.slice(0, maxLength) + suffix;
704
+ };
705
+ };
585
706
  /**
586
707
  * Date filter - formats Date object as localized date string.
587
708
  *
@@ -645,6 +766,27 @@ const ymd = (options) => {
645
766
  return `${year}${opt}${month}${opt}${day}`;
646
767
  };
647
768
  };
769
+ /**
770
+ * Hour-Minute-Second filter - formats Date object as HH:MM:SS string.
771
+ *
772
+ * The counterpart of `ymd`: a fixed, zero-padded, locale-independent rendering with a
773
+ * configurable separator, for when `time` (locale-formatted) is not stable enough.
774
+ *
775
+ * @param options - Array with separator string as first element (default: ':')
776
+ * @returns Filter function that returns formatted time string
777
+ */
778
+ const hms = (options) => {
779
+ const opt = options?.[0] ?? ':';
780
+ return (value) => {
781
+ if (!(value instanceof Date)) {
782
+ valueMustBeDate('hms');
783
+ }
784
+ const hours = value.getHours().toString().padStart(2, '0');
785
+ const minutes = value.getMinutes().toString().padStart(2, '0');
786
+ const seconds = value.getSeconds().toString().padStart(2, '0');
787
+ return `${hours}${opt}${minutes}${opt}${seconds}`;
788
+ };
789
+ };
648
790
  /**
649
791
  * Falsy filter - checks if value is falsy.
650
792
  *
@@ -735,6 +877,8 @@ const builtinFilters = {
735
877
  "mul": mul,
736
878
  "div": div,
737
879
  "mod": mod,
880
+ "abs": abs,
881
+ "clamp": clamp,
738
882
  "fix": fix,
739
883
  "locale": locale,
740
884
  "uc": uc,
@@ -746,16 +890,20 @@ const builtinFilters = {
746
890
  "pad": pad,
747
891
  "rep": rep,
748
892
  "rev": rev,
893
+ "truncate": truncate,
894
+ "join": join,
749
895
  "int": int,
750
896
  "float": float,
751
897
  "round": round,
752
898
  "floor": floor,
753
899
  "ceil": ceil,
754
900
  "percent": percent,
901
+ "unit": unit,
755
902
  "date": date,
756
903
  "time": time,
757
904
  "datetime": datetime,
758
905
  "ymd": ymd,
906
+ "hms": hms,
759
907
  "falsy": falsy,
760
908
  "truthy": truthy,
761
909
  "defaults": defaults,
@@ -793,6 +941,8 @@ const builtinFilterMeta = {
793
941
  mul: { description: "乗算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
794
942
  div: { description: "除算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
795
943
  mod: { description: "剰余", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
944
+ abs: { description: "絶対値", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
945
+ clamp: { description: "範囲内に丸める (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
796
946
  // 数値フォーマット
797
947
  fix: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
798
948
  locale: { description: "ロケール形式で数値フォーマット", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
@@ -806,6 +956,8 @@ const builtinFilterMeta = {
806
956
  pad: { description: "パディング (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
807
957
  rep: { description: "繰り返し (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
808
958
  rev: { description: "文字順を反転", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
959
+ truncate: { description: "切り詰めて省略記号 (length[,suffix])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
960
+ join: { description: "配列を連結 ([separator])", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
809
961
  // 数値パース・丸め
810
962
  int: { description: "整数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
811
963
  float: { description: "浮動小数点数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
@@ -813,11 +965,15 @@ const builtinFilterMeta = {
813
965
  floor: { description: "切り下げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
814
966
  ceil: { description: "切り上げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
815
967
  percent: { description: "パーセンテージ形式", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
968
+ // number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
969
+ // それらは既に string を返すため(builtinFilters.ts の unit を参照)
970
+ unit: { description: "単位(接尾辞)を付加", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
816
971
  // 日付・時刻
817
972
  date: { description: "ロケール形式の日付", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
818
973
  time: { description: "ロケール形式の時刻", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
819
974
  datetime: { description: "ロケール形式の日時", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
820
975
  ymd: { description: "YYYY-MM-DD 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
976
+ hms: { description: "HH:MM:SS 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
821
977
  // 真偽値・変換
822
978
  falsy: { description: "偽値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
823
979
  truthy: { description: "真値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
@@ -870,6 +1026,7 @@ const STATE_COMMAND_NAMESPACE_NAME = "$command";
870
1026
  const STATE_EVENT_TOKENS_NAME = "$eventTokens";
871
1027
  const STATE_ON_NAME = "$on";
872
1028
  const STATE_STREAMS_NAME = "$streams";
1029
+ const STATE_WATCH_NAME = "$watch";
873
1030
  const STATE_LIST_KEYS_NAME = "$listKeys";
874
1031
  const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
875
1032
  const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
@@ -925,6 +1082,7 @@ function getWcsManifest() {
925
1082
  STATE_EVENT_TOKENS_NAME,
926
1083
  STATE_ON_NAME,
927
1084
  STATE_STREAMS_NAME,
1085
+ STATE_WATCH_NAME,
928
1086
  STATE_LIST_KEYS_NAME,
929
1087
  STATE_STREAM_STATUS_NAMESPACE_NAME,
930
1088
  STATE_STREAM_ERROR_NAMESPACE_NAME,
@@ -32,6 +32,8 @@
32
32
  "mul",
33
33
  "div",
34
34
  "mod",
35
+ "abs",
36
+ "clamp",
35
37
  "fix",
36
38
  "locale",
37
39
  "uc",
@@ -43,16 +45,20 @@
43
45
  "pad",
44
46
  "rep",
45
47
  "rev",
48
+ "truncate",
49
+ "join",
46
50
  "int",
47
51
  "float",
48
52
  "round",
49
53
  "floor",
50
54
  "ceil",
51
55
  "percent",
56
+ "unit",
52
57
  "date",
53
58
  "time",
54
59
  "datetime",
55
60
  "ymd",
61
+ "hms",
56
62
  "falsy",
57
63
  "truthy",
58
64
  "defaults",
@@ -215,6 +221,30 @@
215
221
  "number"
216
222
  ]
217
223
  },
224
+ "abs": {
225
+ "description": "絶対値",
226
+ "hasArgs": false,
227
+ "resultType": "number",
228
+ "acceptTypes": [
229
+ "number"
230
+ ],
231
+ "minArgs": 0,
232
+ "maxArgs": 0
233
+ },
234
+ "clamp": {
235
+ "description": "範囲内に丸める (min,max)",
236
+ "hasArgs": true,
237
+ "resultType": "number",
238
+ "acceptTypes": [
239
+ "number"
240
+ ],
241
+ "minArgs": 2,
242
+ "maxArgs": 2,
243
+ "argTypes": [
244
+ "number",
245
+ "number"
246
+ ]
247
+ },
218
248
  "fix": {
219
249
  "description": "固定小数点表記",
220
250
  "hasArgs": true,
@@ -346,6 +376,33 @@
346
376
  "minArgs": 0,
347
377
  "maxArgs": 0
348
378
  },
379
+ "truncate": {
380
+ "description": "切り詰めて省略記号 (length[,suffix])",
381
+ "hasArgs": true,
382
+ "resultType": "string",
383
+ "acceptTypes": [
384
+ "string"
385
+ ],
386
+ "minArgs": 1,
387
+ "maxArgs": 2,
388
+ "argTypes": [
389
+ "number",
390
+ "string"
391
+ ]
392
+ },
393
+ "join": {
394
+ "description": "配列を連結 ([separator])",
395
+ "hasArgs": true,
396
+ "resultType": "string",
397
+ "acceptTypes": [
398
+ "array"
399
+ ],
400
+ "minArgs": 0,
401
+ "maxArgs": 1,
402
+ "argTypes": [
403
+ "string"
404
+ ]
405
+ },
349
406
  "int": {
350
407
  "description": "整数にパース",
351
408
  "hasArgs": false,
@@ -420,6 +477,20 @@
420
477
  "number"
421
478
  ]
422
479
  },
480
+ "unit": {
481
+ "description": "単位(接尾辞)を付加",
482
+ "hasArgs": true,
483
+ "resultType": "string",
484
+ "acceptTypes": [
485
+ "number",
486
+ "string"
487
+ ],
488
+ "minArgs": 1,
489
+ "maxArgs": 1,
490
+ "argTypes": [
491
+ "string"
492
+ ]
493
+ },
423
494
  "date": {
424
495
  "description": "ロケール形式の日付",
425
496
  "hasArgs": false,
@@ -455,6 +526,17 @@
455
526
  "string"
456
527
  ]
457
528
  },
529
+ "hms": {
530
+ "description": "HH:MM:SS 形式",
531
+ "hasArgs": true,
532
+ "resultType": "string",
533
+ "acceptTypes": "any",
534
+ "minArgs": 0,
535
+ "maxArgs": 1,
536
+ "argTypes": [
537
+ "string"
538
+ ]
539
+ },
458
540
  "falsy": {
459
541
  "description": "偽値か判定",
460
542
  "hasArgs": false,
@@ -531,6 +613,7 @@
531
613
  "$eventTokens",
532
614
  "$on",
533
615
  "$streams",
616
+ "$watch",
534
617
  "$listKeys",
535
618
  "$streamStatus",
536
619
  "$streamError"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcstack/state",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Reactive state management with declarative data binding for Web Components. Zero dependencies, buildless.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.esm.js",