@ktfth/stickjs 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/stick-ui/stick-ui.css +275 -40
- package/stick.js +168 -15
package/package.json
CHANGED
package/stick-ui/stick-ui.css
CHANGED
|
@@ -9,21 +9,40 @@
|
|
|
9
9
|
|
|
10
10
|
/* ── Tokens ─────────────────────────────────────────── */
|
|
11
11
|
:root {
|
|
12
|
-
/*
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
12
|
+
/* DARK FIX — semantic tokens (override --sui-* to theme your app) */
|
|
13
|
+
--sui-bg: #ffffff;
|
|
14
|
+
--sui-surface: #f4f4f5;
|
|
15
|
+
--sui-border: #e4e4e7;
|
|
16
|
+
--sui-text: #09090b;
|
|
17
|
+
--sui-text-muted: #71717a;
|
|
18
|
+
--sui-accent: #18181b;
|
|
19
|
+
--sui-accent-fg: #fafafa;
|
|
20
|
+
--sui-success: #22c55e;
|
|
21
|
+
--sui-success-fg: #fafafa;
|
|
22
|
+
--sui-warning: #f59e0b;
|
|
23
|
+
--sui-warning-fg: #09090b;
|
|
24
|
+
--sui-danger: #ef4444;
|
|
25
|
+
--sui-danger-fg: #fafafa;
|
|
26
|
+
--sui-info: #3b82f6;
|
|
27
|
+
--sui-info-fg: #fafafa;
|
|
28
|
+
--sui-ring: #18181b;
|
|
29
|
+
--sui-ring-shadow: rgba(24,24,27,0.1);
|
|
30
|
+
|
|
31
|
+
/* DARK FIX — backward-compatible aliases (--stk-* now reference --sui-*) */
|
|
32
|
+
--stk-bg: var(--sui-bg);
|
|
33
|
+
--stk-fg: var(--sui-text);
|
|
34
|
+
--stk-muted: var(--sui-text-muted);
|
|
35
|
+
--stk-muted-fg: var(--sui-surface);
|
|
36
|
+
--stk-border: var(--sui-border);
|
|
37
|
+
--stk-ring: var(--sui-ring);
|
|
38
|
+
--stk-primary: var(--sui-accent);
|
|
39
|
+
--stk-primary-fg: var(--sui-accent-fg);
|
|
40
|
+
--stk-destructive: var(--sui-danger);
|
|
41
|
+
--stk-destructive-fg: var(--sui-danger-fg);
|
|
42
|
+
--stk-success: var(--sui-success);
|
|
43
|
+
--stk-success-fg: var(--sui-success-fg);
|
|
44
|
+
--stk-warning: var(--sui-warning);
|
|
45
|
+
--stk-warning-fg: var(--sui-warning-fg);
|
|
27
46
|
|
|
28
47
|
/* Radius */
|
|
29
48
|
--stk-radius-sm: 4px;
|
|
@@ -67,31 +86,35 @@
|
|
|
67
86
|
--stk-z-tooltip: 400;
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
/*
|
|
89
|
+
/* DARK FIX — dark mode tokens ──────────────────────── */
|
|
71
90
|
@media (prefers-color-scheme: dark) {
|
|
72
91
|
:root:not([data-theme="light"]) {
|
|
73
|
-
--
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
--
|
|
77
|
-
--
|
|
78
|
-
--
|
|
79
|
-
--
|
|
80
|
-
--
|
|
92
|
+
--sui-bg: #09090b;
|
|
93
|
+
--sui-surface: #27272a;
|
|
94
|
+
--sui-border: #27272a;
|
|
95
|
+
--sui-text: #fafafa;
|
|
96
|
+
--sui-text-muted: #a1a1aa;
|
|
97
|
+
--sui-accent: #fafafa;
|
|
98
|
+
--sui-accent-fg: #18181b;
|
|
99
|
+
--sui-ring: #d4d4d8;
|
|
100
|
+
--sui-ring-shadow: rgba(212,212,216,0.2);
|
|
101
|
+
--sui-info: #60a5fa;
|
|
81
102
|
--stk-shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
|
82
103
|
--stk-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
|
|
83
104
|
--stk-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5);
|
|
84
105
|
}
|
|
85
106
|
}
|
|
86
107
|
[data-theme="dark"] {
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
108
|
+
--sui-bg: #09090b;
|
|
109
|
+
--sui-surface: #27272a;
|
|
110
|
+
--sui-border: #27272a;
|
|
111
|
+
--sui-text: #fafafa;
|
|
112
|
+
--sui-text-muted: #a1a1aa;
|
|
113
|
+
--sui-accent: #fafafa;
|
|
114
|
+
--sui-accent-fg: #18181b;
|
|
115
|
+
--sui-ring: #d4d4d8;
|
|
116
|
+
--sui-ring-shadow: rgba(212,212,216,0.2);
|
|
117
|
+
--sui-info: #60a5fa;
|
|
95
118
|
--stk-shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
|
96
119
|
--stk-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
|
|
97
120
|
--stk-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5);
|
|
@@ -145,14 +168,15 @@
|
|
|
145
168
|
border: 1px solid var(--stk-border); background: var(--stk-bg); color: var(--stk-fg);
|
|
146
169
|
text-decoration: none;
|
|
147
170
|
}
|
|
148
|
-
|
|
171
|
+
/* DARK FIX — explicit color on every :hover prevents dark-on-dark */
|
|
172
|
+
.stk-btn:hover { background: var(--stk-muted-fg); color: var(--stk-fg); }
|
|
149
173
|
.stk-btn:focus-visible { outline: 2px solid var(--stk-ring); outline-offset: 2px; }
|
|
150
174
|
.stk-btn-primary { background: var(--stk-primary); color: var(--stk-primary-fg); border-color: var(--stk-primary); }
|
|
151
|
-
.stk-btn-primary:hover { background: var(--stk-primary); opacity: 0.9; }
|
|
175
|
+
.stk-btn-primary:hover { background: var(--stk-primary); color: var(--stk-primary-fg); opacity: 0.9; }
|
|
152
176
|
.stk-btn-destructive { background: var(--stk-destructive); color: var(--stk-destructive-fg); border-color: var(--stk-destructive); }
|
|
153
|
-
.stk-btn-destructive:hover { background: var(--stk-destructive); opacity: 0.9; }
|
|
177
|
+
.stk-btn-destructive:hover { background: var(--stk-destructive); color: var(--stk-destructive-fg); opacity: 0.9; }
|
|
154
178
|
.stk-btn-ghost { background: transparent; border-color: transparent; }
|
|
155
|
-
.stk-btn-ghost:hover { background: var(--stk-muted-fg); }
|
|
179
|
+
.stk-btn-ghost:hover { background: var(--stk-muted-fg); color: var(--stk-fg); }
|
|
156
180
|
.stk-btn-sm { padding: var(--stk-space-1) var(--stk-space-2); font-size: var(--stk-text-xs); }
|
|
157
181
|
.stk-btn-lg { padding: var(--stk-space-3) var(--stk-space-6); font-size: var(--stk-text-base); }
|
|
158
182
|
|
|
@@ -482,9 +506,11 @@
|
|
|
482
506
|
|
|
483
507
|
.stk-table tbody tr:last-child td { border-bottom: none; }
|
|
484
508
|
|
|
509
|
+
/* DARK FIX — explicit color prevents dark text on dark hover bg */
|
|
485
510
|
.stk-table tbody tr:hover {
|
|
486
511
|
background: var(--stk-muted-fg);
|
|
487
|
-
|
|
512
|
+
color: var(--stk-fg);
|
|
513
|
+
transition: background var(--stk-duration) var(--stk-easing), color var(--stk-duration) var(--stk-easing);
|
|
488
514
|
}
|
|
489
515
|
|
|
490
516
|
/* Sortable headers */
|
|
@@ -539,7 +565,7 @@
|
|
|
539
565
|
.stk-table-filter:focus {
|
|
540
566
|
outline: none;
|
|
541
567
|
border-color: var(--stk-ring);
|
|
542
|
-
box-shadow: 0 0 0 2px
|
|
568
|
+
box-shadow: 0 0 0 2px var(--sui-ring-shadow); /* DARK FIX */
|
|
543
569
|
}
|
|
544
570
|
|
|
545
571
|
/* Pagination bar */
|
|
@@ -716,7 +742,7 @@
|
|
|
716
742
|
.stk-stepper .stk-input:focus {
|
|
717
743
|
outline: none;
|
|
718
744
|
border-color: var(--stk-ring);
|
|
719
|
-
box-shadow: 0 0 0 2px
|
|
745
|
+
box-shadow: 0 0 0 2px var(--sui-ring-shadow); /* DARK FIX */
|
|
720
746
|
}
|
|
721
747
|
|
|
722
748
|
.stk-stepper .stk-label {
|
|
@@ -750,7 +776,7 @@
|
|
|
750
776
|
.stk-autocomplete-input:focus {
|
|
751
777
|
outline: none;
|
|
752
778
|
border-color: var(--stk-ring);
|
|
753
|
-
box-shadow: 0 0 0 2px
|
|
779
|
+
box-shadow: 0 0 0 2px var(--sui-ring-shadow); /* DARK FIX */
|
|
754
780
|
}
|
|
755
781
|
|
|
756
782
|
.stk-autocomplete-dropdown {
|
|
@@ -795,6 +821,215 @@
|
|
|
795
821
|
font-weight: 700;
|
|
796
822
|
}
|
|
797
823
|
|
|
824
|
+
/* ══════════════════════════════════════════════════════
|
|
825
|
+
DARK FIX — Generic element styles & missing components
|
|
826
|
+
══════════════════════════════════════════════════════ */
|
|
827
|
+
|
|
828
|
+
/* ── DARK FIX: Generic dialog base (BUG 4) ─────────── */
|
|
829
|
+
dialog {
|
|
830
|
+
margin: auto;
|
|
831
|
+
position: fixed;
|
|
832
|
+
inset: 0;
|
|
833
|
+
height: fit-content;
|
|
834
|
+
width: min(90%, 420px);
|
|
835
|
+
border: none;
|
|
836
|
+
border-radius: var(--stk-radius-lg, 12px);
|
|
837
|
+
padding: 0;
|
|
838
|
+
background: var(--sui-bg);
|
|
839
|
+
color: var(--sui-text);
|
|
840
|
+
box-shadow: var(--stk-shadow-lg);
|
|
841
|
+
z-index: var(--stk-z-modal, 200);
|
|
842
|
+
}
|
|
843
|
+
dialog::backdrop {
|
|
844
|
+
background: rgba(0,0,0,0.6);
|
|
845
|
+
}
|
|
846
|
+
dialog[open] {
|
|
847
|
+
animation: stk-dialog-in var(--stk-duration-slow, 300ms) var(--stk-easing);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/* ── DARK FIX: Generic form hover/focus (BUG 5) ────── */
|
|
851
|
+
input:hover,
|
|
852
|
+
select:hover,
|
|
853
|
+
textarea:hover {
|
|
854
|
+
color: var(--sui-text);
|
|
855
|
+
background-color: var(--sui-surface);
|
|
856
|
+
}
|
|
857
|
+
input:focus,
|
|
858
|
+
select:focus,
|
|
859
|
+
textarea:focus {
|
|
860
|
+
color: var(--sui-text);
|
|
861
|
+
outline: none;
|
|
862
|
+
border-color: var(--sui-ring);
|
|
863
|
+
box-shadow: 0 0 0 2px var(--sui-ring-shadow); /* DARK FIX */
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/* ── DARK FIX: Generic .btn aliases (BUG 2 / BUG 3) ─ */
|
|
867
|
+
.btn {
|
|
868
|
+
display: inline-flex; align-items: center; justify-content: center; gap: var(--stk-space-2, 8px);
|
|
869
|
+
padding: var(--stk-space-2, 8px) var(--stk-space-4, 16px);
|
|
870
|
+
border-radius: var(--stk-radius-md, 8px);
|
|
871
|
+
font-size: var(--stk-text-sm, 0.875rem); font-weight: 500;
|
|
872
|
+
cursor: pointer;
|
|
873
|
+
transition: all var(--stk-duration, 150ms) var(--stk-easing);
|
|
874
|
+
border: 1px solid var(--sui-border);
|
|
875
|
+
background: var(--sui-bg);
|
|
876
|
+
color: var(--sui-text);
|
|
877
|
+
text-decoration: none;
|
|
878
|
+
font-family: var(--stk-font);
|
|
879
|
+
}
|
|
880
|
+
.btn:hover {
|
|
881
|
+
background: var(--sui-surface);
|
|
882
|
+
color: var(--sui-text);
|
|
883
|
+
}
|
|
884
|
+
.btn:focus-visible {
|
|
885
|
+
outline: 2px solid var(--sui-ring);
|
|
886
|
+
outline-offset: 2px;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.btn-primary {
|
|
890
|
+
background: var(--sui-accent);
|
|
891
|
+
color: var(--sui-accent-fg);
|
|
892
|
+
border-color: var(--sui-accent);
|
|
893
|
+
}
|
|
894
|
+
.btn-primary:hover {
|
|
895
|
+
background: var(--sui-accent);
|
|
896
|
+
color: var(--sui-accent-fg);
|
|
897
|
+
opacity: 0.9;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.btn-danger {
|
|
901
|
+
background: var(--sui-danger);
|
|
902
|
+
color: var(--sui-danger-fg);
|
|
903
|
+
border-color: var(--sui-danger);
|
|
904
|
+
}
|
|
905
|
+
.btn-danger:hover {
|
|
906
|
+
background: var(--sui-danger);
|
|
907
|
+
color: var(--sui-danger-fg);
|
|
908
|
+
opacity: 0.9;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.btn-ghost {
|
|
912
|
+
background: transparent;
|
|
913
|
+
border-color: transparent;
|
|
914
|
+
color: var(--sui-text);
|
|
915
|
+
}
|
|
916
|
+
.btn-ghost:hover {
|
|
917
|
+
background: var(--sui-surface);
|
|
918
|
+
color: var(--sui-text);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.btn-sm { padding: var(--stk-space-1, 4px) var(--stk-space-2, 8px); font-size: var(--stk-text-xs, 0.75rem); }
|
|
922
|
+
.btn-lg { padding: var(--stk-space-3, 12px) var(--stk-space-6, 24px); font-size: var(--stk-text-base, 1rem); }
|
|
923
|
+
|
|
924
|
+
/* ── DARK FIX: Generic table row hover (BUG 1) ─────── */
|
|
925
|
+
table:not(.stk-table) tbody tr:hover td {
|
|
926
|
+
color: var(--sui-text);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* ── DARK FIX: Badge component (BUG 6) ─────────────── */
|
|
930
|
+
.badge, .stk-badge {
|
|
931
|
+
display: inline-flex; align-items: center;
|
|
932
|
+
padding: var(--stk-space-1, 4px) var(--stk-space-2, 8px);
|
|
933
|
+
font-size: var(--stk-text-xs, 0.75rem); font-weight: 500;
|
|
934
|
+
border-radius: var(--stk-radius-full, 9999px);
|
|
935
|
+
line-height: 1.4;
|
|
936
|
+
background: var(--sui-surface);
|
|
937
|
+
color: var(--sui-text);
|
|
938
|
+
}
|
|
939
|
+
.badge-info, .stk-badge-info {
|
|
940
|
+
background: var(--sui-info);
|
|
941
|
+
color: var(--sui-info-fg);
|
|
942
|
+
}
|
|
943
|
+
.badge-success, .stk-badge-success {
|
|
944
|
+
background: var(--sui-success);
|
|
945
|
+
color: var(--sui-success-fg);
|
|
946
|
+
}
|
|
947
|
+
.badge-warning, .stk-badge-warning {
|
|
948
|
+
background: var(--sui-warning);
|
|
949
|
+
color: var(--sui-warning-fg);
|
|
950
|
+
}
|
|
951
|
+
.badge-danger, .stk-badge-danger {
|
|
952
|
+
background: var(--sui-danger);
|
|
953
|
+
color: var(--sui-danger-fg);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* ── DARK FIX: Alert component (BUG 6) ─────────────── */
|
|
957
|
+
.alert, .stk-alert {
|
|
958
|
+
display: flex; align-items: flex-start; gap: var(--stk-space-3, 12px);
|
|
959
|
+
padding: var(--stk-space-3, 12px) var(--stk-space-4, 16px);
|
|
960
|
+
border-radius: var(--stk-radius-md, 8px);
|
|
961
|
+
font-size: var(--stk-text-sm, 0.875rem);
|
|
962
|
+
border: 1px solid var(--sui-border);
|
|
963
|
+
background: var(--sui-bg);
|
|
964
|
+
color: var(--sui-text);
|
|
965
|
+
}
|
|
966
|
+
.alert-success, .stk-alert-success {
|
|
967
|
+
border-left: 3px solid var(--sui-success);
|
|
968
|
+
}
|
|
969
|
+
.alert-danger, .stk-alert-danger {
|
|
970
|
+
border-left: 3px solid var(--sui-danger);
|
|
971
|
+
}
|
|
972
|
+
.alert-warning, .stk-alert-warning {
|
|
973
|
+
border-left: 3px solid var(--sui-warning);
|
|
974
|
+
}
|
|
975
|
+
.alert-info, .stk-alert-info {
|
|
976
|
+
border-left: 3px solid var(--sui-info);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/* ── DARK FIX: Form group component (BUG 6) ────────── */
|
|
980
|
+
.form-group, .stk-form-group {
|
|
981
|
+
display: flex; flex-direction: column;
|
|
982
|
+
gap: var(--stk-space-1, 4px);
|
|
983
|
+
margin-bottom: var(--stk-space-4, 16px);
|
|
984
|
+
}
|
|
985
|
+
.form-group label, .stk-form-group label {
|
|
986
|
+
font-size: var(--stk-text-sm, 0.875rem); font-weight: 500;
|
|
987
|
+
color: var(--sui-text);
|
|
988
|
+
}
|
|
989
|
+
.form-group input,
|
|
990
|
+
.form-group select,
|
|
991
|
+
.form-group textarea,
|
|
992
|
+
.stk-form-group input,
|
|
993
|
+
.stk-form-group select,
|
|
994
|
+
.stk-form-group textarea {
|
|
995
|
+
display: block; width: 100%;
|
|
996
|
+
padding: var(--stk-space-2, 8px) var(--stk-space-3, 12px);
|
|
997
|
+
border: 1px solid var(--sui-border);
|
|
998
|
+
border-radius: var(--stk-radius-md, 8px);
|
|
999
|
+
font-size: var(--stk-text-sm, 0.875rem);
|
|
1000
|
+
font-family: var(--stk-font);
|
|
1001
|
+
color: var(--sui-text);
|
|
1002
|
+
background: var(--sui-bg);
|
|
1003
|
+
transition: border-color var(--stk-duration, 150ms) var(--stk-easing),
|
|
1004
|
+
box-shadow var(--stk-duration, 150ms) var(--stk-easing);
|
|
1005
|
+
}
|
|
1006
|
+
.form-group input::placeholder,
|
|
1007
|
+
.form-group textarea::placeholder,
|
|
1008
|
+
.stk-form-group input::placeholder,
|
|
1009
|
+
.stk-form-group textarea::placeholder {
|
|
1010
|
+
color: var(--sui-text-muted);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* ── DARK FIX: Section header component (BUG 6) ────── */
|
|
1014
|
+
.section-header, .stk-section-header {
|
|
1015
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1016
|
+
gap: var(--stk-space-4, 16px);
|
|
1017
|
+
margin-bottom: var(--stk-space-4, 16px);
|
|
1018
|
+
padding-bottom: var(--stk-space-3, 12px);
|
|
1019
|
+
border-bottom: 1px solid var(--sui-border);
|
|
1020
|
+
}
|
|
1021
|
+
.section-header h1, .section-header h2, .section-header h3,
|
|
1022
|
+
.stk-section-header h1, .stk-section-header h2, .stk-section-header h3 {
|
|
1023
|
+
margin: 0;
|
|
1024
|
+
font-size: var(--stk-text-lg, 1.125rem); font-weight: 600;
|
|
1025
|
+
color: var(--sui-text);
|
|
1026
|
+
}
|
|
1027
|
+
.section-header p, .stk-section-header p {
|
|
1028
|
+
margin: 0;
|
|
1029
|
+
font-size: var(--stk-text-sm, 0.875rem);
|
|
1030
|
+
color: var(--sui-text-muted);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
798
1033
|
.stk-autocomplete-empty {
|
|
799
1034
|
padding: var(--stk-space-4) var(--stk-space-3);
|
|
800
1035
|
text-align: center;
|
package/stick.js
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* data-stick-loading="Loading…" button label while fetch is in-flight
|
|
23
23
|
* data-stick-headers='{"Authorization":"Bearer token"}' fetch headers (JSON)
|
|
24
24
|
* data-stick-json='{"key":"{{value}}"}' send JSON body instead of FormData
|
|
25
|
+
* data-stick-map="#sel:Field, …" JSON field → DOM mapping for fetch-json handler
|
|
26
|
+
* data-stick-then="handler:tgt:p, …" post-fetch action chain (sequential)
|
|
27
|
+
* data-stick-accept="json" parse fetch response as JSON array
|
|
28
|
+
* data-stick-template="#tpl" <template> for JSON array rendering (with accept=json)
|
|
29
|
+
* data-stick-state-loading="…" state actions when fetch starts
|
|
30
|
+
* data-stick-state-done="…" state actions when fetch succeeds
|
|
31
|
+
* data-stick-state-error="…" state actions when fetch fails
|
|
25
32
|
*
|
|
26
33
|
* Target selectors (data-stick-target):
|
|
27
34
|
* "#id" querySelector (default)
|
|
@@ -45,6 +52,7 @@
|
|
|
45
52
|
* {{url:key}} URLSearchParams value from current page URL (e.g. {{url:q}} → ?q=value)
|
|
46
53
|
* {{data-*}} any data attribute, e.g. {{data-id}}
|
|
47
54
|
* {{attr}} any attribute, e.g. {{href}}, {{src}}
|
|
55
|
+
* {{#sel.prop}} cross-element: querySelector(sel).prop (e.g. {{#myInput.value}})
|
|
48
56
|
*
|
|
49
57
|
* Example: data-stick="input:fetch:/api/search?q={{value}}"
|
|
50
58
|
* data-stick="click:dispatch:item-selected:{{data-id}}"
|
|
@@ -80,7 +88,7 @@
|
|
|
80
88
|
}(typeof window !== 'undefined' ? window : this, function () {
|
|
81
89
|
'use strict';
|
|
82
90
|
|
|
83
|
-
const VERSION = '3.
|
|
91
|
+
const VERSION = '3.1.0';
|
|
84
92
|
const handlers = {};
|
|
85
93
|
const listenerMap = new WeakMap(); // el → [{event, fn, options}]
|
|
86
94
|
let _debug = false;
|
|
@@ -135,9 +143,44 @@
|
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
// Resolve {{
|
|
146
|
+
// Resolve {{key}} tokens from a plain object (for JSON template rendering)
|
|
147
|
+
function interpolateFromObj(text, obj) {
|
|
148
|
+
return text.replace(/\{\{([\w-]+)\}\}/g, (_, key) => obj[key] !== undefined ? String(obj[key]) : '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Walk a cloned fragment and interpolate {{tokens}} from a plain object
|
|
152
|
+
function interpolateCloneFromObj(fragment, obj) {
|
|
153
|
+
var walk = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
154
|
+
var node;
|
|
155
|
+
while ((node = walk.nextNode())) {
|
|
156
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
157
|
+
if (node.nodeValue.includes('{{')) node.nodeValue = interpolateFromObj(node.nodeValue, obj);
|
|
158
|
+
} else {
|
|
159
|
+
[...node.attributes].forEach(function(attr) {
|
|
160
|
+
if (attr.value.includes('{{')) attr.value = interpolateFromObj(attr.value, obj);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve {{token}} placeholders — supports cross-element {{#selector.prop}}
|
|
139
167
|
function interpolate(param, el) {
|
|
140
|
-
|
|
168
|
+
// Cross-element interpolation: {{#selector.prop}} resolves from another DOM element
|
|
169
|
+
param = param.replace(/\{\{(#[^}]+)\}\}/g, function(_, ref) {
|
|
170
|
+
var lastDot = ref.lastIndexOf('.');
|
|
171
|
+
if (lastDot <= 0) return '';
|
|
172
|
+
var sel = ref.slice(0, lastDot);
|
|
173
|
+
var prop = ref.slice(lastDot + 1);
|
|
174
|
+
var found = (typeof document !== 'undefined') ? document.querySelector(sel) : null;
|
|
175
|
+
if (!found) return '';
|
|
176
|
+
if (prop === 'value') return found.value ?? '';
|
|
177
|
+
if (prop === 'text' || prop === 'textContent') return found.textContent?.trim() ?? '';
|
|
178
|
+
if (prop === 'checked') return String(found.checked ?? false);
|
|
179
|
+
if (prop.startsWith('data-')) return found.dataset[toCamel(prop.slice(5))] ?? '';
|
|
180
|
+
return found[prop] !== undefined ? String(found[prop]) : (found.getAttribute(prop) ?? '');
|
|
181
|
+
});
|
|
182
|
+
// Local element interpolation
|
|
183
|
+
return param.replace(/\{\{([\w:-]+)\}\}/g, (_, key) => {
|
|
141
184
|
if (key === 'value') return el.value ?? '';
|
|
142
185
|
if (key === 'text') return el.textContent.trim();
|
|
143
186
|
if (key === 'id') return el.id ?? '';
|
|
@@ -224,6 +267,57 @@
|
|
|
224
267
|
});
|
|
225
268
|
}
|
|
226
269
|
|
|
270
|
+
// ── state & then helpers ────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
// Parse state action: "handler:param:#target" or "handler:#target"
|
|
273
|
+
function parseStateAction(str) {
|
|
274
|
+
str = str.trim();
|
|
275
|
+
if (!str) return null;
|
|
276
|
+
var parts = str.split(':');
|
|
277
|
+
if (parts.length < 2) return null;
|
|
278
|
+
return { handler: parts[0], targetSel: parts[parts.length - 1].trim(), param: parts.length > 2 ? parts.slice(1, -1).join(':').trim() : '' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Apply state actions (Feature 5) — e.g. "add-class:loading:#container, disable:#btn"
|
|
282
|
+
function applyStateActions(actionsStr) {
|
|
283
|
+
if (!actionsStr) return;
|
|
284
|
+
actionsStr.split(',').forEach(function(raw) {
|
|
285
|
+
var parsed = parseStateAction(raw);
|
|
286
|
+
if (!parsed) return;
|
|
287
|
+
var target = document.querySelector(parsed.targetSel);
|
|
288
|
+
if (!target || typeof handlers[parsed.handler] !== 'function') return;
|
|
289
|
+
try { handlers[parsed.handler](target, parsed.param, { type: 'state' }, target); }
|
|
290
|
+
catch (err) { console.error('[Stick:state]', err); }
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Parse then action: "handler:target:param" or "handler:target"
|
|
295
|
+
function parseThenEntry(str) {
|
|
296
|
+
str = str.trim();
|
|
297
|
+
if (!str) return null;
|
|
298
|
+
var i1 = str.indexOf(':');
|
|
299
|
+
if (i1 === -1) return { handler: str, targetSel: null, param: '' };
|
|
300
|
+
var rest = str.slice(i1 + 1);
|
|
301
|
+
var i2 = rest.indexOf(':');
|
|
302
|
+
if (i2 === -1) return { handler: str.slice(0, i1), targetSel: rest.trim(), param: '' };
|
|
303
|
+
return { handler: str.slice(0, i1), targetSel: rest.slice(0, i2).trim(), param: rest.slice(i2 + 1).trim() };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Execute post-fetch then chain (Feature 2) — "handler:target:param, …"
|
|
307
|
+
async function executeThenChain(actionsStr, sourceEl) {
|
|
308
|
+
if (!actionsStr) return;
|
|
309
|
+
var actions = actionsStr.split(',');
|
|
310
|
+
for (var i = 0; i < actions.length; i++) {
|
|
311
|
+
var parsed = parseThenEntry(actions[i]);
|
|
312
|
+
if (!parsed) continue;
|
|
313
|
+
var target = parsed.targetSel ? document.querySelector(parsed.targetSel) : sourceEl;
|
|
314
|
+
if (!target) { console.warn('[Stick:then] target not found:', parsed.targetSel); continue; }
|
|
315
|
+
if (typeof handlers[parsed.handler] !== 'function') { console.warn('[Stick:then] unknown handler:', parsed.handler); continue; }
|
|
316
|
+
try { await handlers[parsed.handler](sourceEl, parsed.param, { type: 'then' }, target); }
|
|
317
|
+
catch (err) { console.error('[Stick:then] handler "' + parsed.handler + '" threw:', err); }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
227
321
|
// ── core ──────────────────────────────────────────────────────────
|
|
228
322
|
|
|
229
323
|
const Stick = {
|
|
@@ -597,15 +691,17 @@
|
|
|
597
691
|
const val = localStorage.getItem(p);
|
|
598
692
|
if (val !== null) t.value !== undefined ? (t.value = val) : (t.textContent = val);
|
|
599
693
|
})
|
|
600
|
-
// Network
|
|
694
|
+
// Network — supports accept=json (F4), state lifecycle (F5), then chain (F2)
|
|
601
695
|
.add('fetch', async (el, param, e, target) => {
|
|
602
696
|
e?.preventDefault?.();
|
|
603
|
-
const method
|
|
604
|
-
const swapMode
|
|
605
|
-
const loading
|
|
606
|
-
const errorSel
|
|
607
|
-
const errorEl
|
|
608
|
-
const prev
|
|
697
|
+
const method = (el.dataset.stickMethod || 'GET').toUpperCase();
|
|
698
|
+
const swapMode = el.dataset.stickSwap || 'innerHTML';
|
|
699
|
+
const loading = el.dataset.stickLoading || '';
|
|
700
|
+
const errorSel = el.dataset.stickError;
|
|
701
|
+
const errorEl = errorSel ? document.querySelector(errorSel) : null;
|
|
702
|
+
const prev = el.textContent;
|
|
703
|
+
const acceptJson = el.dataset.stickAccept === 'json';
|
|
704
|
+
const templateSel = el.dataset.stickTemplate;
|
|
609
705
|
|
|
610
706
|
let headers = {};
|
|
611
707
|
try { headers = JSON.parse(el.dataset.stickHeaders || '{}'); } catch (_) {}
|
|
@@ -613,6 +709,7 @@
|
|
|
613
709
|
if (loading) el.textContent = loading;
|
|
614
710
|
el.setAttribute('aria-busy', 'true');
|
|
615
711
|
if (errorEl) errorEl.hidden = true;
|
|
712
|
+
applyStateActions(el.dataset.stickStateLoading);
|
|
616
713
|
|
|
617
714
|
try {
|
|
618
715
|
let body;
|
|
@@ -625,19 +722,75 @@
|
|
|
625
722
|
body = new FormData(el.closest('form') || el);
|
|
626
723
|
}
|
|
627
724
|
}
|
|
628
|
-
const res
|
|
725
|
+
const res = await fetch(param, { method, body, headers });
|
|
629
726
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
727
|
+
|
|
728
|
+
if (acceptJson) {
|
|
729
|
+
// Feature 4: JSON response → clone template per item into target
|
|
730
|
+
const json = await res.json();
|
|
731
|
+
const tpl = templateSel ? document.querySelector(templateSel) : null;
|
|
732
|
+
if (tpl && tpl.tagName === 'TEMPLATE') {
|
|
733
|
+
target.innerHTML = '';
|
|
734
|
+
var items = Array.isArray(json) ? json : [json];
|
|
735
|
+
items.forEach(function(item) {
|
|
736
|
+
var clone = tpl.content.cloneNode(true);
|
|
737
|
+
interpolateCloneFromObj(clone, item);
|
|
738
|
+
target.appendChild(clone);
|
|
739
|
+
});
|
|
740
|
+
Stick.init(target);
|
|
741
|
+
} else {
|
|
742
|
+
console.warn('[Stick:fetch] accept=json requires data-stick-template pointing to a <template>');
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
const text = await res.text();
|
|
746
|
+
swap(target, text, swapMode);
|
|
747
|
+
Stick.init(target);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
applyStateActions(el.dataset.stickStateDone);
|
|
751
|
+
await executeThenChain(el.dataset.stickThen, el);
|
|
633
752
|
} catch (err) {
|
|
634
753
|
console.error('[Stick:fetch]', err);
|
|
754
|
+
applyStateActions(el.dataset.stickStateError);
|
|
635
755
|
if (errorEl) { errorEl.textContent = err.message; errorEl.hidden = false; }
|
|
636
|
-
else target.textContent = `Error: ${err.message}`;
|
|
756
|
+
else if (!el.dataset.stickStateError) target.textContent = `Error: ${err.message}`;
|
|
637
757
|
} finally {
|
|
638
758
|
if (loading) el.textContent = prev;
|
|
639
759
|
el.removeAttribute('aria-busy');
|
|
640
760
|
}
|
|
761
|
+
})
|
|
762
|
+
// Feature 1: fetch-json — GET URL, map JSON fields to DOM elements via data-stick-map
|
|
763
|
+
.add('fetch-json', async (el, param, e, target) => {
|
|
764
|
+
var mapStr = el.dataset.stickMap;
|
|
765
|
+
el.setAttribute('aria-busy', 'true');
|
|
766
|
+
applyStateActions(el.dataset.stickStateLoading);
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
var res = await fetch(param, { method: 'GET' });
|
|
770
|
+
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
771
|
+
var json = await res.json();
|
|
772
|
+
|
|
773
|
+
if (mapStr) {
|
|
774
|
+
mapStr.split(',').forEach(function(entry) {
|
|
775
|
+
entry = entry.trim();
|
|
776
|
+
if (!entry) return;
|
|
777
|
+
var idx = entry.indexOf(':');
|
|
778
|
+
if (idx === -1) return;
|
|
779
|
+
var sel = entry.slice(0, idx).trim();
|
|
780
|
+
var field = entry.slice(idx + 1).trim();
|
|
781
|
+
var found = document.querySelector(sel);
|
|
782
|
+
if (found && json[field] !== undefined) found.textContent = String(json[field]);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
applyStateActions(el.dataset.stickStateDone);
|
|
787
|
+
await executeThenChain(el.dataset.stickThen, el);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
console.error('[Stick:fetch-json]', err);
|
|
790
|
+
applyStateActions(el.dataset.stickStateError);
|
|
791
|
+
} finally {
|
|
792
|
+
el.removeAttribute('aria-busy');
|
|
793
|
+
}
|
|
641
794
|
});
|
|
642
795
|
|
|
643
796
|
// ── auto-init ─────────────────────────────────────────────────────
|