Compare commits
1017 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
7830ea7bae | |
![]() |
9b76b0ae44 | |
![]() |
f1fa1cba71 | |
![]() |
3afa2c098e | |
![]() |
47a095f04a | |
![]() |
3fe8157a09 | |
![]() |
104e77e1e8 | |
![]() |
6be5ad2fbc | |
![]() |
10b29846ae | |
![]() |
6b2d488ae6 | |
![]() |
83c120a3b1 | |
![]() |
d81ea1aef9 | |
![]() |
3c5974c2e1 | |
![]() |
b758fb3e06 | |
![]() |
a705eb7044 | |
![]() |
a4145207f0 | |
![]() |
439c71341b | |
![]() |
c4a99869cb | |
![]() |
aa138d5a19 | |
![]() |
3869f09f5a | |
![]() |
686d12ad2a | |
![]() |
5a894a0139 | |
![]() |
7b706e126b | |
![]() |
16fa3cdd90 | |
![]() |
8b84986410 | |
![]() |
3b18b23a1f | |
![]() |
c780ded239 | |
![]() |
72a3e28fea | |
![]() |
23da089235 | |
![]() |
ffc98aa905 | |
![]() |
44bd907f44 | |
![]() |
83b4af8299 | |
![]() |
7f7b555998 | |
![]() |
3379e822fb | |
![]() |
f1e0f80922 | |
![]() |
ccc7ea427a | |
![]() |
9c36bae809 | |
![]() |
5f10252adb | |
![]() |
aed0b3a228 | |
![]() |
5ba9759fba | |
![]() |
05c751770b | |
![]() |
024111f67e | |
![]() |
4cc46524d8 | |
![]() |
e422bbdecc | |
![]() |
00d4e87a01 | |
![]() |
f754ada278 | |
![]() |
48e7a124a4 | |
![]() |
dba88cb2fb | |
![]() |
3c34e03b3d | |
![]() |
41f87ae7b2 | |
![]() |
67ed84ca2a | |
![]() |
b676dea6eb | |
![]() |
a18dbfc539 | |
![]() |
d7fba2787e | |
![]() |
e28bb58eee | |
![]() |
09bd26ccd2 | |
![]() |
ea74aa41a5 | |
![]() |
2534471565 | |
![]() |
433bb6e5ab | |
![]() |
c6b1eaca30 | |
![]() |
e991742383 | |
![]() |
90b8b1cf35 | |
![]() |
d8dacf01d7 | |
![]() |
59c5221b7a | |
![]() |
db376dfbe4 | |
![]() |
55a3f01318 | |
![]() |
a98f3b78ad | |
![]() |
07fd37c9e5 | |
![]() |
6311779c40 | |
![]() |
56011be1ce | |
![]() |
08b5ac5e3a | |
![]() |
5a3ee74bf5 | |
![]() |
666a45827e | |
![]() |
1bd119caf6 | |
![]() |
c35b85ce4e | |
![]() |
39cb56db3e | |
![]() |
9fbd96caed | |
![]() |
0ada59177b | |
![]() |
1cb3d0118a | |
![]() |
3a561c2faa | |
![]() |
92a7a92939 | |
![]() |
ab1d5f11b1 | |
![]() |
d898f92cb9 | |
![]() |
2d1cf0ce54 | |
![]() |
0201fc6a2e | |
![]() |
70376416fc | |
![]() |
be186f3849 | |
![]() |
f17c99d50f | |
![]() |
1c1e52ccce | |
![]() |
d3aac79e98 | |
![]() |
986ed894c1 | |
![]() |
65ea56a2e2 | |
![]() |
100e4ac64c | |
![]() |
bf1a64d697 | |
![]() |
bceefa2122 | |
![]() |
685c033cf4 | |
![]() |
eb825e2b7d | |
![]() |
4322b99559 | |
![]() |
4c0bf6dfbd | |
![]() |
750883a2d1 | |
![]() |
94a691e059 | |
![]() |
bebd836f45 | |
![]() |
d60f326230 | |
![]() |
7d20db71f6 | |
![]() |
42acbff4a2 | |
![]() |
182610e046 | |
![]() |
72e1c2c07e | |
![]() |
69bf92b634 | |
![]() |
256dad1ba6 | |
![]() |
750cd44740 | |
![]() |
2925230e34 | |
![]() |
d8b22b58fa | |
![]() |
3d16cabd5a | |
![]() |
45db7d9d29 | |
![]() |
978598f52d | |
![]() |
8f1a8d6b75 | |
![]() |
0d07c77ef4 | |
![]() |
42a2384666 | |
![]() |
edc0b5b8d3 | |
![]() |
641774ea66 | |
![]() |
e40db72ed5 | |
![]() |
a7df17505f | |
![]() |
cab1456bd1 | |
![]() |
8685255430 | |
![]() |
14dab8a652 | |
![]() |
e44f446eea | |
![]() |
ff56adcc40 | |
![]() |
57a0f468ac | |
![]() |
116a1362f9 | |
![]() |
a454c450c6 | |
![]() |
73d86f27b7 | |
![]() |
c6567508e3 | |
![]() |
87b200f764 | |
![]() |
624d24b2f6 | |
![]() |
3cf9802bae | |
![]() |
1e5c238690 | |
![]() |
41ee5b7de5 | |
![]() |
2c0cc510e1 | |
![]() |
9e12b9076c | |
![]() |
391de46e08 | |
![]() |
a4c2e87473 | |
![]() |
2db7468cd5 | |
![]() |
32d4f920f6 | |
![]() |
0942459599 | |
![]() |
17f22018ba | |
![]() |
0c085c320d | |
![]() |
e77a68f6a6 | |
![]() |
657c1c8492 | |
![]() |
440ac29fb2 | |
![]() |
4f84035efc | |
![]() |
b7caf5e41c | |
![]() |
1e7f0dd8d2 | |
![]() |
8f17404aae | |
![]() |
2012286f8f | |
![]() |
821bdfd7ee | |
![]() |
bc395f4932 | |
![]() |
d245ab10a5 | |
![]() |
7a24033982 | |
![]() |
7ee8d30e11 | |
![]() |
3bc1f28019 | |
![]() |
b85a6b1241 | |
![]() |
c86c69d072 | |
![]() |
a9c8e6ed11 | |
![]() |
03d36e849c | |
![]() |
de567de237 | |
![]() |
f500e93183 | |
![]() |
d20737a75c | |
![]() |
9cda19db84 | |
![]() |
47afc9e01e | |
![]() |
3cd4a5d079 | |
![]() |
23134b4b8e | |
![]() |
0d544431ed | |
![]() |
fbea7b1d71 | |
![]() |
e90319dad3 | |
![]() |
5e06342a3a | |
![]() |
a44f86d051 | |
![]() |
1e9c7a820b | |
![]() |
2a16577829 | |
![]() |
2f8cfea9ba | |
![]() |
ee1b2e460d | |
![]() |
e822f2c416 | |
![]() |
8b1c46bd7b | |
![]() |
f36327c36d | |
![]() |
8b4e750986 | |
![]() |
6b2130b1bf | |
![]() |
18a69f44ce | |
![]() |
54dab9a2a7 | |
![]() |
6e677ad352 | |
![]() |
faaf52f5ee | |
![]() |
b9dc373e63 | |
![]() |
f87279552a | |
![]() |
2fd150f56c | |
![]() |
752b91d9b9 | |
![]() |
e4ad61c5c5 | |
![]() |
defedfa44d | |
![]() |
5c890ecf09 | |
![]() |
7c696b15a3 | |
![]() |
0e3a4c980b | |
![]() |
5ea8fd7fc4 | |
![]() |
781ea7764b | |
![]() |
14b2dd351a | |
![]() |
dcf1d00810 | |
![]() |
2b5e59c418 | |
![]() |
a685ec5d5b | |
![]() |
8a9a7f2630 | |
![]() |
f29f3fb98c | |
![]() |
b3a5dbbfdb | |
![]() |
a76d3ecb2d | |
![]() |
fa679aa40c | |
![]() |
e89639050d | |
![]() |
cc75d774e9 | |
![]() |
86e07b3a6b | |
![]() |
1acef4efc3 | |
![]() |
ad47bdfb85 | |
![]() |
ea71691061 | |
![]() |
0480644851 | |
![]() |
dc3c406346 | |
![]() |
c8a7e58b50 | |
![]() |
31b0c51a76 | |
![]() |
afa40c83aa | |
![]() |
2e9570f0ae | |
![]() |
0a00bed483 | |
![]() |
bbdb8d6652 | |
![]() |
4042dc0e25 | |
![]() |
860aa9e46d | |
![]() |
c89da30a6d | |
![]() |
1004107c93 | |
![]() |
9945394a79 | |
![]() |
8a0fedf30b | |
![]() |
ed9f5748ac | |
![]() |
65b017d65f | |
![]() |
c90c09e741 | |
![]() |
ef05be33a8 | |
![]() |
6d16a23999 | |
![]() |
16714ed0ad | |
![]() |
91f5f07413 | |
![]() |
305b8eca89 | |
![]() |
4fb2aa5b6a | |
![]() |
bd17ab4809 | |
![]() |
01702f0fde | |
![]() |
18ea30972e | |
![]() |
467aeb4312 | |
![]() |
b874aa16e2 | |
![]() |
e1fb273cd8 | |
![]() |
7314357d2e | |
![]() |
39a4e1553c | |
![]() |
229183f136 | |
![]() |
3e5f1aaa09 | |
![]() |
0d08df81ec | |
![]() |
ead432fe04 | |
![]() |
b6319e9b1d | |
![]() |
c0adef6eea | |
![]() |
84a31ca6a0 | |
![]() |
d97cd5f40a | |
![]() |
4aafd4262f | |
![]() |
d45517ffbb | |
![]() |
32a14d985a | |
![]() |
5d46096dc2 | |
![]() |
ca5414c358 | |
![]() |
5a4b4d699e | |
![]() |
bc21b05164 | |
![]() |
d3b50bc3a9 | |
![]() |
7a994bdff3 | |
![]() |
1d74d2328a | |
![]() |
552c55939d | |
![]() |
437f8bcf53 | |
![]() |
0c33439fa6 | |
![]() |
b4a11683fb | |
![]() |
71b95c8949 | |
![]() |
0975e24224 | |
![]() |
b00c6d160e | |
![]() |
65a301da9e | |
![]() |
bca69f6a57 | |
![]() |
ff52c94fb5 | |
![]() |
41198f3479 | |
![]() |
77700d2e2f | |
![]() |
9b61ee4f67 | |
![]() |
284c42d981 | |
![]() |
6b726a340e | |
![]() |
9901ae7f4d | |
![]() |
fe409baf4c | |
![]() |
cbbb640533 | |
![]() |
97c0760a48 | |
![]() |
1d8105af11 | |
![]() |
38c0547dfa | |
![]() |
93852fc1ad | |
![]() |
539152a127 | |
![]() |
ed980b3d4c | |
![]() |
854ce6ed30 | |
![]() |
16161d8309 | |
![]() |
652fd1e130 | |
![]() |
2ee3ad21a4 | |
![]() |
9b2d620499 | |
![]() |
dbff015dc9 | |
![]() |
fb8325c583 | |
![]() |
3f95087783 | |
![]() |
eb8b3b1c5b | |
![]() |
c25cbec470 | |
![]() |
701370246f | |
![]() |
d7b527f39e | |
![]() |
51671c666d | |
![]() |
a5c2e8624b | |
![]() |
31d2080741 | |
![]() |
93e8e1405c | |
![]() |
e5ccf1242d | |
![]() |
34556125ae | |
![]() |
62fe77741b | |
![]() |
65a84c6602 | |
![]() |
2234f3926f | |
![]() |
d1fe079520 | |
![]() |
805993a9b7 | |
![]() |
ba6fba2b25 | |
![]() |
0e241426e9 | |
![]() |
1e55dcf6d5 | |
![]() |
f333707293 | |
![]() |
50472e2782 | |
![]() |
ca6c591aef | |
![]() |
39a93a4d50 | |
![]() |
afa6e930fa | |
![]() |
c8e77a619d | |
![]() |
d4f11e77aa | |
![]() |
a9cfaf8a78 | |
![]() |
c40f43bc9a | |
![]() |
a2e4181450 | |
![]() |
7385cb0abf | |
![]() |
490a26fa78 | |
![]() |
a1fcf54752 | |
![]() |
820f1795fa | |
![]() |
4708c24da0 | |
![]() |
ea1c1326fe | |
![]() |
433c2e0cac | |
![]() |
929c32af4e | |
![]() |
420559dd66 | |
![]() |
78f1adeab9 | |
![]() |
ee5150fa4e | |
![]() |
5244040041 | |
![]() |
9b5a788ada | |
![]() |
7036bffb0f | |
![]() |
65beaaab41 | |
![]() |
4bafc8b250 | |
![]() |
3850a0cb84 | |
![]() |
1177a2d3ab | |
![]() |
44d758a1ca | |
![]() |
46773b5602 | |
![]() |
cf7807b39b | |
![]() |
081178321f | |
![]() |
43449e1e3a | |
![]() |
c825bacb3c | |
![]() |
b51853d464 | |
![]() |
efd38eaf8d | |
![]() |
f082567d8e | |
![]() |
2b767131cf | |
![]() |
29e7f9fcab | |
![]() |
b91d183b8c | |
![]() |
72375ae91c | |
![]() |
30743468dd | |
![]() |
f57703254d | |
![]() |
451b62ef1b | |
![]() |
937c7ccec4 | |
![]() |
0fa34d99ce | |
![]() |
c78114b960 | |
![]() |
5992480a97 | |
![]() |
55ec85bc9e | |
![]() |
3992f254d7 | |
![]() |
1ac2d8ac79 | |
![]() |
44004a89e5 | |
![]() |
0f2d428cbc | |
![]() |
07b01321f7 | |
![]() |
fdb41f3a0d | |
![]() |
ceabb783e6 | |
![]() |
9244e530ae | |
![]() |
326f039e03 | |
![]() |
3a65035b0a | |
![]() |
c4b2c2364b | |
![]() |
7c8e2551b4 | |
![]() |
2e2ef7ddec | |
![]() |
91483ab6ff | |
![]() |
e99c9e0654 | |
![]() |
aa45410ed1 | |
![]() |
a79fb963b3 | |
![]() |
b218e2115d | |
![]() |
8b43f29224 | |
![]() |
4d6ca33b23 | |
![]() |
e9d0885c6d | |
![]() |
ea19292f95 | |
![]() |
48151b593c | |
![]() |
723fca3395 | |
![]() |
0a67e27077 | |
![]() |
76da175516 | |
![]() |
1ea565a34f | |
![]() |
d1f3e19acb | |
![]() |
7824c07223 | |
![]() |
ffbec7025d | |
![]() |
fa24bd44e4 | |
![]() |
9572b867a3 | |
![]() |
161d9fedc7 | |
![]() |
c36a7227b8 | |
![]() |
6b59265dc1 | |
![]() |
eee4e3c981 | |
![]() |
9f88878f04 | |
![]() |
9a31383e2f | |
![]() |
e149f38be1 | |
![]() |
34d95080f3 | |
![]() |
39849bf23a | |
![]() |
12273efe44 | |
![]() |
ddcc835b8a | |
![]() |
5736b4ae75 | |
![]() |
337b81a8d8 | |
![]() |
e2c24d20c3 | |
![]() |
ae4b836431 | |
![]() |
1dcbab9558 | |
![]() |
f6bc599b85 | |
![]() |
e43fe54b83 | |
![]() |
11c2c86a30 | |
![]() |
60db7e2959 | |
![]() |
81a9b4cf31 | |
![]() |
753412539d | |
![]() |
953df608fe | |
![]() |
0ed8abf34d | |
![]() |
e1d30aebdb | |
![]() |
13da358729 | |
![]() |
755a5db9b8 | |
![]() |
cf60cab386 | |
![]() |
a706cebb2d | |
![]() |
d851e049eb | |
![]() |
192900c7eb | |
![]() |
beef6b3602 | |
![]() |
3c6c29f3b9 | |
![]() |
c0bdb00f39 | |
![]() |
0875d74810 | |
![]() |
19ad9ceada | |
![]() |
5defce5cc1 | |
![]() |
9db6d03dc8 | |
![]() |
1aeb613970 | |
![]() |
e5c6ec4b3d | |
![]() |
ac9a44b803 | |
![]() |
3cf498e171 | |
![]() |
d12241a812 | |
![]() |
182765419c | |
![]() |
05eaa113cd | |
![]() |
0483fa80c2 | |
![]() |
4f70926469 | |
![]() |
f02da62231 | |
![]() |
4d27ea199b | |
![]() |
891764569c | |
![]() |
6fa8900f68 | |
![]() |
5030f1428e | |
![]() |
f72e78ba6f | |
![]() |
7934512a99 | |
![]() |
ae2f033926 | |
![]() |
dfb650011c | |
![]() |
c47574f70c | |
![]() |
bcf016b538 | |
![]() |
e96e4a216c | |
![]() |
0e6c6baeb6 | |
![]() |
f72d906ba2 | |
![]() |
f8bb417405 | |
![]() |
b74e2bfbb8 | |
![]() |
fdb00bdcb4 | |
![]() |
ba3c10fde7 | |
![]() |
a22dafc057 | |
![]() |
daf00c49c0 | |
![]() |
843e5848ef | |
![]() |
abff1d05de | |
![]() |
ff6cdce6d1 | |
![]() |
ff09c67a0e | |
![]() |
cac77adf27 | |
![]() |
6e11ff8f74 | |
![]() |
05121d96f2 | |
![]() |
1d7a2009fd | |
![]() |
5d1b59a2b4 | |
![]() |
07c21e92ed | |
![]() |
bffbcf47d6 | |
![]() |
9a21366110 | |
![]() |
86b65595e2 | |
![]() |
72fefafe7a | |
![]() |
2243144a52 | |
![]() |
6a999c2d7e | |
![]() |
ae5791fb5f | |
![]() |
b3f9a18a4d | |
![]() |
30e47767a5 | |
![]() |
51c79816dc | |
![]() |
9dbece08f5 | |
![]() |
f7fb1c8af0 | |
![]() |
161c2dbfc9 | |
![]() |
49e7153131 | |
![]() |
945c213320 | |
![]() |
1a0b9d29e5 | |
![]() |
702a81af58 | |
![]() |
1325eec587 | |
![]() |
c8fc7fd238 | |
![]() |
48fce4f01c | |
![]() |
170824e772 | |
![]() |
5d52df0ba2 | |
![]() |
47043504ab | |
![]() |
72b28b2a97 | |
![]() |
519db9a954 | |
![]() |
56ae15a56f | |
![]() |
e446b3eedc | |
![]() |
44f114c53f | |
![]() |
b1de35a4f6 | |
![]() |
d2c56ef730 | |
![]() |
6412661082 | |
![]() |
949e56f3e0 | |
![]() |
6b501be16c | |
![]() |
a7aabb49ff | |
![]() |
de45cdfabf | |
![]() |
66c0aa4fcf | |
![]() |
f1bfa080dc | |
![]() |
b411e9ed72 | |
![]() |
dc41488154 | |
![]() |
2103f602ae | |
![]() |
46960ec505 | |
![]() |
2ad2457c93 | |
![]() |
31e3648cc0 | |
![]() |
685da484ad | |
![]() |
b3750b832a | |
![]() |
1f0a93fe00 | |
![]() |
910c604045 | |
![]() |
c3370186b5 | |
![]() |
761a0fb3d6 | |
![]() |
b4f6372c84 | |
![]() |
82369bc516 | |
![]() |
b42c8c2c88 | |
![]() |
8c5edc7ee7 | |
![]() |
d11108f933 | |
![]() |
67a05b0779 | |
![]() |
56059c5019 | |
![]() |
192c27c59b | |
![]() |
77e71c75f5 | |
![]() |
f8e93f1ac9 | |
![]() |
ba6ef70d40 | |
![]() |
040cfe3059 | |
![]() |
0ea0f3cf37 | |
![]() |
daa5c0496c | |
![]() |
634e28b594 | |
![]() |
fed87f584b | |
![]() |
d341409758 | |
![]() |
c02f056d96 | |
![]() |
d99bdc3c39 | |
![]() |
94e81ec04e | |
![]() |
2f8533cd0b | |
![]() |
7b9e60a834 | |
![]() |
c6dbc96153 | |
![]() |
1f1cd4bd4c | |
![]() |
e594204813 | |
![]() |
12953172ac | |
![]() |
2b64f9da03 | |
![]() |
062753e1f5 | |
![]() |
35aaa72c64 | |
![]() |
bb76da59cc | |
![]() |
494f0d4191 | |
![]() |
636b203455 | |
![]() |
dbfdd77a94 | |
![]() |
a2fe65a9a1 | |
![]() |
9d6443921b | |
![]() |
d4a7d4af44 | |
![]() |
cd9ea6d4ce | |
![]() |
f649ab1bd3 | |
![]() |
2f3bb704ca | |
![]() |
0c7ab4f921 | |
![]() |
dc2768d44f | |
![]() |
9ca42b340e | |
![]() |
4c73f53690 | |
![]() |
9069f510a8 | |
![]() |
dcf1a39092 | |
![]() |
72fbff06ec | |
![]() |
791651073a | |
![]() |
6fca8a7c72 | |
![]() |
dcffccd3ae | |
![]() |
f2db306540 | |
![]() |
40ba9f1268 | |
![]() |
d9a2e6cef0 | |
![]() |
0568bd7214 | |
![]() |
777fb9b41f | |
![]() |
66dd1c1d7b | |
![]() |
c302246cd9 | |
![]() |
76789e8f0d | |
![]() |
e33cd26f45 | |
![]() |
c5ba1a9c8e | |
![]() |
dbdfffc1c1 | |
![]() |
57c7f2d8ac | |
![]() |
fe67174373 | |
![]() |
35a4f55562 | |
![]() |
dc06f31b77 | |
![]() |
2c87ee3bfa | |
![]() |
34a04770be | |
![]() |
2c02121ed4 | |
![]() |
4b7993d409 | |
![]() |
941109e55b | |
![]() |
1c15dc1453 | |
![]() |
940f09b3c4 | |
![]() |
418cc3a80f | |
![]() |
f15bd3b87a | |
![]() |
436e4ddbd8 | |
![]() |
6ccc9e9c94 | |
![]() |
9a0e74112a | |
![]() |
f0327340fa | |
![]() |
1f823de4a1 | |
![]() |
424cfd87ba | |
![]() |
edf9ec6143 | |
![]() |
79041854e0 | |
![]() |
80abc3a667 | |
![]() |
6a591acdcf | |
![]() |
f8780010ae | |
![]() |
2a14ac7ebe | |
![]() |
8b7fe09c1f | |
![]() |
0b4725caec | |
![]() |
b59db9e980 | |
![]() |
ebd3e23575 | |
![]() |
8624f45276 | |
![]() |
e6138a2698 | |
![]() |
6a1c061715 | |
![]() |
e8671d8c88 | |
![]() |
2be4e5ef7b | |
![]() |
20ad2ff9ac | |
![]() |
326445dc43 | |
![]() |
fffdc0fe64 | |
![]() |
0a5e19eaa8 | |
![]() |
35bfc89a42 | |
![]() |
0551551c62 | |
![]() |
c5389419ae | |
![]() |
6f0293dec4 | |
![]() |
0892104623 | |
![]() |
6b8110671a | |
![]() |
9ca90807ad | |
![]() |
b3ec534a5d | |
![]() |
82d669a42e | |
![]() |
e3dd27e10b | |
![]() |
b26e4c1415 | |
![]() |
6172fbd43d | |
![]() |
d6826ca92d | |
![]() |
4c20476b47 | |
![]() |
2fe3713f39 | |
![]() |
255a699f7d | |
![]() |
41fb2df3a8 | |
![]() |
14839abfb5 | |
![]() |
75701f70d6 | |
![]() |
208b94aefb | |
![]() |
afcd51126c | |
![]() |
09a5c9017c | |
![]() |
cbdae3de93 | |
![]() |
7c0c37fb0f | |
![]() |
21749ef0d3 | |
![]() |
1000e2e95a | |
![]() |
80728114b8 | |
![]() |
39268eda93 | |
![]() |
f50a04ae4f | |
![]() |
2759382e7c | |
![]() |
3595708a40 | |
![]() |
26d0e5ca20 | |
![]() |
06fb18fb3a | |
![]() |
b6128b5dde | |
![]() |
57dd46aaad | |
![]() |
eeb0e03d0a | |
![]() |
328577fc2a | |
![]() |
bd34006c42 | |
![]() |
bac182f5d6 | |
![]() |
5d7afc0245 | |
![]() |
fff41075d4 | |
![]() |
5708dfb20f | |
![]() |
a6e9fe6e71 | |
![]() |
0b7fbcf341 | |
![]() |
c104da0371 | |
![]() |
885830304b | |
![]() |
1a129e3951 | |
![]() |
ce895445e0 | |
![]() |
378468ee56 | |
![]() |
abd96a9139 | |
![]() |
48a8ff572f | |
![]() |
8caecab717 | |
![]() |
bb3a0a718a | |
![]() |
2546b3a624 | |
![]() |
78de4e3014 | |
![]() |
887b9f7293 | |
![]() |
ccdd0d79db | |
![]() |
df109f91d2 | |
![]() |
093aeac21a | |
![]() |
ee215e5fc0 | |
![]() |
d5350b0b5b | |
![]() |
fdc73d5d9c | |
![]() |
57be12a943 | |
![]() |
d165916b49 | |
![]() |
c2c7dea7cd | |
![]() |
42a596f753 | |
![]() |
75068e68e4 | |
![]() |
2ff246c7d2 | |
![]() |
5cfec4ea7a | |
![]() |
559436c092 | |
![]() |
d6e744632e | |
![]() |
38d038dd4d | |
![]() |
bdac0f31e8 | |
![]() |
d61381f7c4 | |
![]() |
6899db0c19 | |
![]() |
093b4bdcfa | |
![]() |
e1874e3bfc | |
![]() |
66d44bc7f3 | |
![]() |
23aafbaa56 | |
![]() |
c0d8fdb583 | |
![]() |
0fa2405728 | |
![]() |
3fe8e6ce2e | |
![]() |
1b541e5e35 | |
![]() |
26b947b8d1 | |
![]() |
1b4b228f48 | |
![]() |
ca1e3b5888 | |
![]() |
837832f3d0 | |
![]() |
b3d8e50edc | |
![]() |
f0eea03171 | |
![]() |
63482a9912 | |
![]() |
f3d04d5385 | |
![]() |
37b201622b | |
![]() |
1679ad2d45 | |
![]() |
7934b385bd | |
![]() |
206a3c13f8 | |
![]() |
4219f2ed58 | |
![]() |
030f93048b | |
![]() |
99f8ef2a05 | |
![]() |
1714295db7 | |
![]() |
ba10b96f10 | |
![]() |
a74e754d96 | |
![]() |
12f8b4d5db | |
![]() |
42e061cb1b | |
![]() |
c496647ebe | |
![]() |
3a6eee09a1 | |
![]() |
93679364d0 | |
![]() |
c303ac0da0 | |
![]() |
8380a7224b | |
![]() |
5b14f9f174 | |
![]() |
2cce474a85 | |
![]() |
a69f019731 | |
![]() |
18c9a457e1 | |
![]() |
99cc5c9640 | |
![]() |
17c01af5e2 | |
![]() |
387bbd8c40 | |
![]() |
2d53463b6e | |
![]() |
fee32181cc | |
![]() |
f87ad44151 | |
![]() |
fbc5eeaae3 | |
![]() |
0fa9d93913 | |
![]() |
600fca2ff0 | |
![]() |
a9bc6f5aa0 | |
![]() |
a02c0f15a9 | |
![]() |
303beaf7da | |
![]() |
8ebedce27f | |
![]() |
07d8628f4a | |
![]() |
207f1205b2 | |
![]() |
5283522909 | |
![]() |
e1c3590698 | |
![]() |
636e87fe14 | |
![]() |
836de8b2f3 | |
![]() |
8aac732c28 | |
![]() |
8a91477cae | |
![]() |
1e99ba988a | |
![]() |
acba200de8 | |
![]() |
b0fde462de | |
![]() |
29282a7983 | |
![]() |
4d29fdf78b | |
![]() |
37444e2305 | |
![]() |
91b58933e7 | |
![]() |
b9bee9623e | |
![]() |
4c78e43460 | |
![]() |
95b31b0453 | |
![]() |
5eaac19ec4 | |
![]() |
9af0ffae44 | |
![]() |
8895df60c3 | |
![]() |
cdbbf6af92 | |
![]() |
bca5eba9d7 | |
![]() |
321079e3ae | |
![]() |
35ff45604a | |
![]() |
3a55047a08 | |
![]() |
b481d60313 | |
![]() |
269fdfd131 | |
![]() |
b027422eaf | |
![]() |
3b1a79b82e | |
![]() |
48134fe0ff | |
![]() |
787bb7044f | |
![]() |
afa0ec5bd6 | |
![]() |
00107ab323 | |
![]() |
ddbd0804d1 | |
![]() |
f696d693ac | |
![]() |
9e61275e1c | |
![]() |
e1ecf57198 | |
![]() |
f4d0ecf328 | |
![]() |
299424b0f4 | |
![]() |
7b798fe7c4 | |
![]() |
c5d024bde5 | |
![]() |
f2f8c1271d | |
![]() |
b09cacee33 | |
![]() |
150a961d33 | |
![]() |
5764b53eb9 | |
![]() |
802bf0309f | |
![]() |
345d417041 | |
![]() |
55d68879f5 | |
![]() |
21857de541 | |
![]() |
a19821ff36 | |
![]() |
94b8c6a494 | |
![]() |
41b8d05dff | |
![]() |
af1205a298 | |
![]() |
6b5b3a62a6 | |
![]() |
379730c21a | |
![]() |
e3da825170 | |
![]() |
53431df523 | |
![]() |
df7cd11f13 | |
![]() |
3fea94bbb6 | |
![]() |
b6b0593891 | |
![]() |
909fd4cdaf | |
![]() |
706344fa14 | |
![]() |
a623df49bd | |
![]() |
8ccc8bd33a | |
![]() |
4fc8a74b86 | |
![]() |
c6b711177e | |
![]() |
67ef5a0211 | |
![]() |
78cd6d94df | |
![]() |
5ddb55b0ad | |
![]() |
839446db67 | |
![]() |
559a10bfea | |
![]() |
1e6558168e | |
![]() |
816e3410a1 | |
![]() |
1c4eff6e21 | |
![]() |
d3a939ed8c | |
![]() |
b7baab20b7 | |
![]() |
fe71830ffc | |
![]() |
3ba49ebe18 | |
![]() |
25940275fd | |
![]() |
3d6ec86881 | |
![]() |
2b7be994e0 | |
![]() |
dcf572def2 | |
![]() |
2c752ac328 | |
![]() |
f8f18494c9 | |
![]() |
d35a053cea | |
![]() |
978411f619 | |
![]() |
28f0aa3933 | |
![]() |
b61bcea05e | |
![]() |
ce0c4e9e4f | |
![]() |
b27e7a105f | |
![]() |
acebb6977f | |
![]() |
4a91f8eb20 | |
![]() |
0be1ed6057 | |
![]() |
975752cff1 | |
![]() |
784af7543a | |
![]() |
d8a3e5d5da | |
![]() |
74b637889d | |
![]() |
34fffab63d | |
![]() |
ddf281fd16 | |
![]() |
a92aa3f6b9 | |
![]() |
38d51ab355 | |
![]() |
a74f4be540 | |
![]() |
3da57f1c30 | |
![]() |
afec038131 | |
![]() |
3643084b1b | |
![]() |
714e4bca20 | |
![]() |
4f92bc7545 | |
![]() |
0a577e3e8e | |
![]() |
c21d9d390d | |
![]() |
00b8e6ba8f | |
![]() |
91e5e432ea | |
![]() |
59e70fc7bd | |
![]() |
0dc938e4bb | |
![]() |
45223e9728 | |
![]() |
37cc8555a7 | |
![]() |
39df52f344 | |
![]() |
d15c821ceb | |
![]() |
d9552e5143 | |
![]() |
686f4ad4ae | |
![]() |
7e4cbd91c6 | |
![]() |
fd20dd9869 | |
![]() |
54dcd69a36 | |
![]() |
59f1ce8732 | |
![]() |
322ddbf646 | |
![]() |
fe46056a18 | |
![]() |
740eef306e | |
![]() |
311b13779d | |
![]() |
aedbf553c9 | |
![]() |
1350755474 | |
![]() |
dcf7dbabdd | |
![]() |
352cdc3e23 | |
![]() |
79c2e3a397 | |
![]() |
9a07618ac4 | |
![]() |
9f3ffc685f | |
![]() |
d84c653e84 | |
![]() |
ad644574f8 | |
![]() |
a9ae426c13 | |
![]() |
880e44f1b8 | |
![]() |
1ad6a7c4c6 | |
![]() |
823ce74401 | |
![]() |
1d274b181e | |
![]() |
75d2bfc416 | |
![]() |
defeddc9b2 | |
![]() |
e15b0cfa44 | |
![]() |
ca2e064206 | |
![]() |
797c4b6bd2 | |
![]() |
0bad12cc5f | |
![]() |
a6d1e6a914 | |
![]() |
3e54e533d1 | |
![]() |
85285df468 | |
![]() |
3eb2eb210b | |
![]() |
63f164685b | |
![]() |
4cb5ca5f63 | |
![]() |
8fc53b14a9 | |
![]() |
4276889448 | |
![]() |
ab0bc92946 | |
![]() |
805748209a | |
![]() |
28719283c0 | |
![]() |
f16163080d | |
![]() |
0bce22f9c2 | |
![]() |
578cb14922 | |
![]() |
9c9340e76e | |
![]() |
b32804ebf5 | |
![]() |
72680008f0 | |
![]() |
3951381f08 | |
![]() |
ba6edcf359 | |
![]() |
c4605ef411 | |
![]() |
e5bf5be13c | |
![]() |
1eb5b5d8c8 | |
![]() |
13d11ece39 | |
![]() |
92837cacac | |
![]() |
38e301e9f2 | |
![]() |
89b3228c87 | |
![]() |
1cbdb6e985 | |
![]() |
4914e9d4bf | |
![]() |
b3bd304321 | |
![]() |
44ef446c33 | |
![]() |
962e8fe0b9 | |
![]() |
5c0865d4a6 | |
![]() |
2dcecfcca8 | |
![]() |
e721a5261d | |
![]() |
c53bc221e1 | |
![]() |
64aea9aba3 | |
![]() |
8a3db108e5 | |
![]() |
6add96537e | |
![]() |
e1b6bdcdbc | |
![]() |
e1c164ba9a | |
![]() |
d0a396136a | |
![]() |
effc4edaa3 | |
![]() |
434189464a | |
![]() |
71cac90d9e | |
![]() |
cd51af92a2 | |
![]() |
bebbdcf556 | |
![]() |
7a06c65939 | |
![]() |
b0243e99c7 | |
![]() |
1cd9f396a7 | |
![]() |
6ca0a5b3d4 | |
![]() |
7222691493 | |
![]() |
f1392fc6f0 | |
![]() |
3c85a1321c | |
![]() |
98d177630f | |
![]() |
27ee6aa5d7 | |
![]() |
c66bd8b5fb | |
![]() |
f608cc65e1 | |
![]() |
c084402921 | |
![]() |
94838262af | |
![]() |
457c90204a | |
![]() |
350ceb9585 | |
![]() |
be67208a72 | |
![]() |
e98e951342 | |
![]() |
688d136740 | |
![]() |
0c9a00c2bf | |
![]() |
3b71ac7af9 | |
![]() |
1bcb4564f8 | |
![]() |
14c55a225f | |
![]() |
2e7f28f88f | |
![]() |
61db33f779 | |
![]() |
4a65bef0ed | |
![]() |
496c94a3aa | |
![]() |
a3af7bf07d | |
![]() |
d1e7d7ab18 | |
![]() |
8dce4ff728 | |
![]() |
9c315d7b47 | |
![]() |
657fc04149 | |
![]() |
3b2b54b479 | |
![]() |
78c63320d5 | |
![]() |
d30a0a9f64 | |
![]() |
9b53eac285 | |
![]() |
2b0a8f05ec | |
![]() |
c3c69ff9f8 | |
![]() |
9622e659d8 | |
![]() |
440ee21dd6 | |
![]() |
9cf9615bac | |
![]() |
3b22e2612a | |
![]() |
b4d011c9d3 | |
![]() |
9c34156fda | |
![]() |
953396d4d5 | |
![]() |
12b0b69fe5 | |
![]() |
58222160bb | |
![]() |
d64041fc5d | |
![]() |
8d2826365e | |
![]() |
789ebb1f61 | |
![]() |
6153d6aff2 | |
![]() |
27074e88cb | |
![]() |
12bc908f79 | |
![]() |
11460ff360 | |
![]() |
14d70e7713 | |
![]() |
ccce8472eb | |
![]() |
f754c6e13f | |
![]() |
301ab4fa6a | |
![]() |
9548859549 | |
![]() |
1e16745c4c | |
![]() |
7baca616f3 | |
![]() |
a714085efe | |
![]() |
2814c79adb | |
![]() |
d10ddb37bb | |
![]() |
ef9b91d3a4 | |
![]() |
078bf2982e | |
![]() |
db5dba5a6f | |
![]() |
080344083b | |
![]() |
c267f130a2 | |
![]() |
d733eb2382 | |
![]() |
e6cd4643cf | |
![]() |
4e20aa59db | |
![]() |
cbb1a8534e | |
![]() |
e0eb04802f | |
![]() |
8c391e2bd6 | |
![]() |
926f463688 | |
![]() |
cbaba21eae | |
![]() |
7e11992605 | |
![]() |
a5d48a3e12 | |
![]() |
6411135d10 |
|
@ -1,36 +0,0 @@
|
|||
# flyctl launch added from .gitignore
|
||||
**/debug_bin*
|
||||
**/gen
|
||||
**/node_modules
|
||||
|
||||
# flyctl launch added from library/.gitignore
|
||||
# Logs
|
||||
library/**/logs
|
||||
library/**/*.log
|
||||
library/**/npm-debug.log*
|
||||
library/**/yarn-debug.log*
|
||||
library/**/yarn-error.log*
|
||||
library/**/pnpm-debug.log*
|
||||
library/**/lerna-debug.log*
|
||||
|
||||
library/**/node_modules
|
||||
library/**/dist
|
||||
library/**/dist-ssr
|
||||
library/**/*.local
|
||||
|
||||
# Editor directories and files
|
||||
library/**/stats.html
|
||||
|
||||
# flyctl launch added from library/node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/.gitignore
|
||||
!library/node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/**/*
|
||||
|
||||
# flyctl launch added from library/node_modules/.pnpm/vite-plugin-webfont-dl@3.7.6_vite@4.4.5/node_modules/vite-plugin-webfont-dl/dist/.gitignore
|
||||
library/node_modules/.pnpm/vite-plugin-webfont-dl@3.7.6_vite@4.4.5/node_modules/vite-plugin-webfont-dl/dist/**/*
|
||||
!library/node_modules/.pnpm/vite-plugin-webfont-dl@3.7.6_vite@4.4.5/node_modules/vite-plugin-webfont-dl/dist/**/.gitignore
|
||||
|
||||
# flyctl launch added from node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/.gitignore
|
||||
!node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/**/*
|
||||
|
||||
# flyctl launch added from webui/assetgen/css/node_modules/.pnpm/tailwindcss@3.3.2/node_modules/tailwindcss/stubs/.gitignore
|
||||
!webui/assetgen/css/node_modules/.pnpm/tailwindcss@3.3.2/node_modules/tailwindcss/stubs/**/*
|
||||
fly.toml
|
|
@ -1,8 +1,2 @@
|
|||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.blend* filter=lfs diff=lfs merge=lfs -text
|
||||
*.hdr* filter=lfs diff=lfs merge=lfs -text
|
||||
*.wav* filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf* filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
bundles linguist-generated=true
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
* @delaneyj @bencroker
|
|
@ -0,0 +1,31 @@
|
|||
name: Bug Report
|
||||
description: Create a bug report for Datastar.
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
So you think you’ve discovered a bug in Datastar? To ensure this hasn’t already been reported, please first search [existing issues](https://github.com/starfederation/datastar/issues?q=is%3Aissue) and ensure you’re running the [latest release](https://github.com/starfederation/datastar/releases). The `#bug-report` channel in the [Discord server](https://discord.gg/bnRNgZjgPh) is also a great place to report bugs.
|
||||
|
||||
To rule out that this isn’t a setup or configuration issue, please read the [guide](https://data-star.dev/guide) and the [reference](https://data-star.dev/reference/attribute_plugins) docs.
|
||||
|
||||
If you still believe you’ve found a bug, please provide a clear and concise description, including:
|
||||
- What is happening and what you expected to happen.
|
||||
- Steps to reproduce the issue.
|
||||
- Screenshots, if applicable.
|
||||
- type: textarea
|
||||
id: body
|
||||
attributes:
|
||||
label: Bug Report
|
||||
description: Please provide a clear and concise description of the bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Datastar Version
|
||||
description: Provide the Datastar version that this relates to, ideally the [latest release](https://github.com/starfederation/datastar/releases).
|
||||
placeholder: v1.0.0
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
|
@ -0,0 +1,26 @@
|
|||
name: Feature Request
|
||||
description: Suggest a new feature for Datastar.
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
So you have a feature request for Datastar? To ensure this isn’t already on our radar, please first search [existing issues](https://github.com/starfederation/datastar/issues?q=is%3Aissue). The `#general` channel in the [Discord server](https://discord.gg/bnRNgZjgPh) is also a great place to suggest features.
|
||||
|
||||
If you still believe you have a new suggestion, please provide a clear and concise description, including:
|
||||
- What problem you believe your feature request can solve.
|
||||
- Any alternative solutions or features you have already considered.
|
||||
- type: textarea
|
||||
id: body
|
||||
attributes:
|
||||
label: Feature Request
|
||||
description: Please provide a clear and concise suggestion for a feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Datastar Version
|
||||
description: Provide the Datastar version that this relates to, ideally the [latest release](https://github.com/starfederation/datastar/releases).
|
||||
placeholder: v1.0.0
|
|
@ -0,0 +1,24 @@
|
|||
name: Support Request
|
||||
description: Ask a question about Datastar.
|
||||
labels:
|
||||
- question
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
So you have a question about Datastar? To ensure your question hasn’t already been answered, please first search [existing issues](https://github.com/starfederation/datastar/issues?q=is%3Aissue). The `#help` channel in the [Discord server](https://discord.gg/bnRNgZjgPh) is also a great place to ask questions.
|
||||
|
||||
If your question is still unanswered, please provide a clear and concise description. Note that we are diligent about documentation, so please check whether your question is answered in the [guide](https://data-star.dev/guide) or the [reference](https://data-star.dev/reference/attribute_plugins) docs before submitting.
|
||||
- type: textarea
|
||||
id: body
|
||||
attributes:
|
||||
label: Support Request
|
||||
description: Please provide a clear and concise question.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Datastar Version
|
||||
description: Provide the Datastar version that this relates to, ideally the [latest release](https://github.com/starfederation/datastar/releases).
|
||||
placeholder: v1.0.0
|
|
@ -0,0 +1,34 @@
|
|||
name: Build Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: develop
|
||||
paths:
|
||||
- 'Dockerfile-dev'
|
||||
- '.github/workflows/build-docker-images.yml'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-dev
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./.github
|
||||
file: ./Dockerfile-dev
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
build-args: |
|
||||
ENVIRONMENT=development
|
|
@ -0,0 +1,29 @@
|
|||
name: Deploy Site
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy app
|
||||
if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[deploy-site]')
|
||||
runs-on: ubuntu-latest
|
||||
concurrency: deploy-group
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Deploy to fly.io
|
||||
uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
- run: |
|
||||
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task tools'
|
||||
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task support'
|
||||
flyctl deploy --local-only
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
|
@ -0,0 +1,23 @@
|
|||
name: Enforce Branch Policy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
enforce-branch-policy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Enforce main branch pull request policy
|
||||
if: github.event_name == 'pull_request' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo "Pull requests to the main branch are not allowed."
|
||||
exit 1
|
||||
- name: Enforce main branch push policy
|
||||
run: |
|
||||
echo "Push events to the main branch are allowed."
|
||||
exit 0
|
|
@ -0,0 +1,45 @@
|
|||
name: Build & Publish NuGet to GitHub Registry
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NuGetDirectory: ${{ github.workspace }}/nuget
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Build and Pack NuGet package with versioning
|
||||
run: dotnet pack sdk/dotnet/src/Datastar.fsproj --configuration Release --output ${{ env.NuGetDirectory }}
|
||||
|
||||
- name: Upload NuGet package to GitHub
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nugetPackage
|
||||
path: ${{ env.NuGetDirectory }}/*.nupkg
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
|
||||
- name: Download nuget package artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nugetPackage
|
||||
path: nugetPackage
|
||||
|
||||
- name: Publish to NuGet.org
|
||||
run: dotnet nuget push nugetPackage/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
|
@ -0,0 +1,30 @@
|
|||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- 'bundles/*.js'
|
||||
- 'library/src/**/*.ts'
|
||||
- 'site/smoketests/*.go'
|
||||
- 'site/static/md/tests/*.md'
|
||||
- '.github/workflows/tests.yml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run CI tests
|
||||
run: |
|
||||
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task tools'
|
||||
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task test'
|
|
@ -1,4 +1,17 @@
|
|||
debug_bin*
|
||||
tailwindcli
|
||||
datastar_site
|
||||
data
|
||||
.task
|
||||
.idea
|
||||
.DS_Store
|
||||
node_modules
|
||||
datastar-website
|
||||
*_bin
|
||||
*.qtpl.go
|
||||
*/java/*/target/
|
||||
*.pyc
|
||||
__pycache__
|
||||
__debug_bin*
|
||||
|
||||
site_bin
|
||||
# search index
|
||||
/data-star.bleve
|
|
@ -1 +0,0 @@
|
|||
2bfcd1f864211ed9e253e325659f416
|
|
@ -1 +0,0 @@
|
|||
f0e21a76375460d3b7824608ee23c2d0
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"jdinabox.quicktemplate-vscode",
|
||||
"golang.go",
|
||||
"a-h.templ"
|
||||
]
|
||||
}
|
|
@ -9,14 +9,16 @@
|
|||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/backends/go/cmd/site/main.go"
|
||||
"program": "${workspaceFolder}/site/cmd/site/main.go",
|
||||
"preLaunchTask": "build datastar"
|
||||
},
|
||||
{
|
||||
"name": "Go test example",
|
||||
"name": "Build Library",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/backends/example/go/main.go"
|
||||
"program": "${workspaceFolder}/build/cmd/build/main.go",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,20 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"datastar",
|
||||
"HTMX",
|
||||
"hyperscript",
|
||||
"idiomorph",
|
||||
"Neato",
|
||||
"sudodevnull",
|
||||
"TLDR",
|
||||
"Websockets"
|
||||
],
|
||||
"playwright.workspaceSettings": {
|
||||
"configs": [
|
||||
{
|
||||
"relativeConfigFile": "playwright/playwright.config.ts",
|
||||
"selected": true,
|
||||
"enabled": true,
|
||||
"projects": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"go.testTimeout": "200s",
|
||||
"go.coverOnSingleTestFile": true,
|
||||
"go.coverOnSingleTest": true,
|
||||
"editor.foldingStrategy": "indentation",
|
||||
"makefile.configureOnOpen": false,
|
||||
"editor.formatOnSave": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.formatOnSave": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "build datastar",
|
||||
"command": "task",
|
||||
"args": [
|
||||
"support"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# Building Datastar using Docker
|
||||
|
||||
Datastar comes with a development environment for developing Datastar via a Docker container.
|
||||
|
||||
It takes care of the setup listed here for you in a Dockerized environment, which allows you to get it up and running quickly & easily.
|
||||
|
||||
See [DOCKER.md](DOCKER.md) for detailed instructions on using it.
|
||||
|
||||
# Building Datastar Natively
|
||||
|
||||
To run the examples website you will need the following:
|
||||
|
||||
- [Go](https://go.dev/)
|
||||
- [Taskfile](https://taskfile.dev/)
|
||||
|
||||
To publish the library or if you're using a BSD operating system, you will also need:
|
||||
|
||||
- [pnpm](https://pnpm.io/)
|
||||
- [Node and npm](https://nodejs.org/)
|
||||
|
||||
`task tools` will install all required tools
|
||||
|
||||
`task build` will build the library as well as sdk constant templates
|
||||
|
||||
The compiled files can be found in `./bundles`
|
||||
|
||||
Finally, `task` will build the library and serve the reference documentation website.
|
||||
|
||||
The website should be available on http://localhost:8080 with hot-reloading enabled.
|
||||
|
||||
## End to end tests
|
||||
|
||||
Once you have the website running, you should be able to run the
|
||||
[rod](https://go-rod.github.io) tests.
|
||||
|
||||
```
|
||||
$ task test
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
# Release Notes for Datastar
|
||||
|
||||
Each tagged version of Datastar is accompanied by a release note. Read the [release notes »](https://github.com/starfederation/datastar/releases)
|
||||
|
||||
# WIP Release Notes
|
||||
|
||||
## v1.0.0-beta.11
|
||||
|
||||
### Added
|
||||
|
||||
- Added the `__debounce`, `__throttle` and `__viewtransition` modifiers to `data-on-intersect`.
|
||||
- Added the `__debounce`, `__throttle` and `__viewtransition` modifiers to `data-on-signal-change`.
|
||||
- Added the `__viewtransition` modifier to `data-on-interval`.
|
||||
- Added the `__viewtransition` modifier to `data-on-load`.
|
||||
- Added the `__viewtransition` modifier to `data-on-raf`.
|
||||
|
||||
### Changed
|
||||
|
||||
- The `datastar-sse` event is now dispatched on the `document` element, and using `data-on-datastar-sse` automatically listens for the event on the `document` ([#802](https://github.com/starfederation/datastar/issues/802)).
|
||||
- The `data-on-signals-change-*` attribute key now accepts a path in which `*` matches a single path segment and `**` matches multiple path segments (`data-on-signals-change-foo.*.baz`, `data-on-signals-change-foo.**`).
|
||||
- The `data-persist` attribute now accepts one or more space-separated paths in which `*` matches a single path segment and `**` matches multiple path segments (`data-persist="foo.*.baz"`, `data-persist="foo.**"`).
|
||||
- The `@setAll` action now accepts one or more space-separated paths in which `*` matches a single path segment and `**` matches multiple path segments (`@setAll('foo.*.baz', true)`, `@setAll('foo.**', true)`) ([#793](https://github.com/starfederation/datastar/issues/793)).
|
||||
- The `@toggleAll` action now accepts one or more space-separated paths in which `*` matches a single path segment and `**` matches multiple path segments (`@toggleAll('foo.*.baz')`, `@toggleAll('foo.**')`) ([#793](https://github.com/starfederation/datastar/issues/793)).
|
|
@ -0,0 +1,39 @@
|
|||
# Contributing to Datastar
|
||||
|
||||
Thanks for thinking about contributing to Datastar 🚀
|
||||
|
||||
## Before You Contribute
|
||||
|
||||
Before you contribute, please consider that Datastar is a lightweight JavaScript framework that aims to simplify reactive web apps by using a minimalist, attribute-based approach for state management and event handling directly in HTML.
|
||||
|
||||
Anything outside of this scope may not be accepted. If you have a need for a feature that is not within the scope of Datastar, consider using a Web Component or writing your own Datastar plugin.
|
||||
|
||||
## Bug Reports & Feature Requests
|
||||
|
||||
Before submitting bug reports and feature requests, please search the [open issues](https://github.com/starfederation/datastar/issues) and the _#help_ channel in the [Discord server](https://discord.gg/bnRNgZjgPh) to see if it has already been addressed. When submitting a [new issue](https://github.com/starfederation/datastar/issues/new), please use a descriptive title and include a clear description and as much relevant information as possible.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Always open an issue to discuss your idea _before_ submitting a pull request. Pull requests must be made to the `develop` branch, have a descriptive title and clearly describe the problem and solution. If the pull request adds or changes behavior, documentation and examples should be updated accordingly.
|
||||
|
||||
## Documentation
|
||||
|
||||
Datastar’s documentation is written in markdown files that live in [this folder](https://github.com/starfederation/datastar/tree/develop/site/static/md). Improvements to them can be submitted via pull requests.
|
||||
|
||||
## SDKs
|
||||
|
||||
We are currently only accepting SDKs from people willing to contribute _and_ maintain them.
|
||||
|
||||
SDKs must follow the [Architecture Decision Record](https://github.com/starfederation/datastar/blob/develop/sdk/README.md). Pull requests for new SDKs must also include the following (use alphabetical order, where appropriate):
|
||||
|
||||
- A README file that explains how to use the SDK.
|
||||
- An entry in the [SDKs reference guide](https://github.com/starfederation/datastar/blob/develop/site/static/md/reference/sdks.md).
|
||||
- A value in `SDKLanguages` in [`consts.go`](https://github.com/starfederation/datastar/blob/develop/build/consts.go).
|
||||
- A `consts_[language].qtpl` file for generating [constants and defaults](https://github.com/starfederation/datastar/blob/develop/build).
|
||||
- Sample [code snippets](https://github.com/starfederation/datastar/tree/develop/site/static/code_snippets) in the language of your SDK.
|
||||
- An implementation of each of the [examples](https://github.com/starfederation/datastar/tree/develop/examples).
|
||||
- Template values for consts and examples in [`run.go`](https://github.com/starfederation/datastar/blob/develop/build/run.go).
|
||||
|
||||
## Building
|
||||
|
||||
Datastar comes with instructions for building, either manually or via Docker. Read the [Building Guidelines »](BUILDING.md)
|
|
@ -0,0 +1,60 @@
|
|||
# Building Datastar using Docker
|
||||
|
||||
Datastar comes with a development environment for developing Datastar via a Docker container.
|
||||
|
||||
It takes care of the setup listed in the for you in a Dockerized environment, which allows you to get it up and running quickly & easily.
|
||||
|
||||
The only requirement is that you have [Docker](https://www.docker.com/products/docker-desktop) installed (you do not need `golang`, `tmpl`, `npm`, `pnpm` or any other project dependencies installed locally).
|
||||
|
||||
## Why Docker?
|
||||
|
||||
Developers who have adopted [Docker](https://www.docker.com/) for a containerized approach to development are used to not having to install a matching development infrastructure each time they approach a project.
|
||||
|
||||
This allows you to "shrink-wrap" the devops needed to run a project in a container, which will run anywhere, on any machine, without having to do any meticulous setup.
|
||||
|
||||
It also allows you to easily swap between basic things like Go versions, without affecting your local computer.
|
||||
|
||||
## Quick Start
|
||||
|
||||
In terminal, `cd` to the `datastar/` directory, and then type:
|
||||
|
||||
```
|
||||
make dev
|
||||
```
|
||||
|
||||
The first time you run this command, it may take a bit of time to build the Docker image, and download all of the appropriate packages, and cache them locally.
|
||||
|
||||
Then just navigate to `http://localhost:8080` in your browser, and Datastar site from `site` will be up and running.
|
||||
|
||||
You can freely make changes to the `library` Datastar codebase, and the changes will be rebuilt and reload automatically.
|
||||
|
||||
You can also make changes to the `site` Datastar website backend, and the backend will be rebuilt and reload automatically.
|
||||
|
||||
## Using Datastar Docker Dev
|
||||
|
||||
Datastar Docker Dev uses the venerable `make` command to automate setup and access to the Docker containers used.
|
||||
|
||||
It uses `make` because it's available pre-installed on any development machine.
|
||||
|
||||
See the [Using Make & Makefiles to Automate your Frontend Workflow](https://nystudio107.com/blog/using-make-makefiles-to-automate-your-frontend-workflow) article for more on `make`.
|
||||
|
||||
The make tool is available for just about every platform you can imagine, and is installed with the [XCode CLI Tools](https://www.embarcadero.com/starthere/xe5/mobdevsetup/ios/en/installing_the_commandline_tools.html) on the Mac, and [WSL2](https://docs.microsoft.com/en-us/windows/wsl/install-win10) on Windows. Probably you have these installed already if you’re doing development.
|
||||
|
||||
Below are details and options available in each of the provided `make` commands:
|
||||
|
||||
* `make dev` - starts up the Go website server for the backend with hot reloading as you make changes
|
||||
* `make test` - run the project tests inside the Docker container
|
||||
* `make task xxx` - runs the specified task from the `Taskfile.yml` inside of the Docker container
|
||||
* `make ssh` - gives you a shell inside of the Docker container
|
||||
* `make image-build` - rebuilds the Docker image from scratch (you will probably never need this)
|
||||
* `make clean` - removes all build artifacts, including the Docker image & volumes, to start from scratch
|
||||
|
||||
### CLI Arguments
|
||||
|
||||
You can pass in optional CLI arguments to override the default settings Datastar dev uses:
|
||||
|
||||
* `TAG=` (default is defined in `Dockerfile-dev`) - allows you to specify the official [golang Docker image](https://hub.docker.com/_/golang) tag that should be used. Using this, you can change the version of Go the container runs, e.g.: `make image-build TAG="1.24"` will use the latest patch version of Go 1.24 official Docker image.
|
||||
|
||||
### Terminating
|
||||
|
||||
To terminate the `datastar-dev` Docker container, enter `Control-C` in the terminal.
|
13
Dockerfile
13
Dockerfile
|
@ -1,16 +1,17 @@
|
|||
FROM docker.io/golang:1.22.1-alpine AS build
|
||||
FROM docker.io/golang:1.24.2-alpine AS build
|
||||
|
||||
RUN apk add --no-cache upx
|
||||
ENV PORT=8080
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.* *.go ./
|
||||
COPY . .
|
||||
RUN go mod download
|
||||
COPY backends/go/. ./backends/go/
|
||||
COPY site ./site
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -o /out/site backends/go/cmd/site/main.go
|
||||
RUN upx /out/site
|
||||
go build -ldflags="-s" -o /out/site site/cmd/site/main.go
|
||||
RUN upx -9 -k /out/site
|
||||
|
||||
FROM scratch
|
||||
FROM alpine
|
||||
RUN chmod a=rwx,u+t /tmp
|
||||
COPY --from=build /out/site /
|
||||
ENTRYPOINT ["/site"]
|
|
@ -0,0 +1,48 @@
|
|||
ARG TAG=1.24
|
||||
|
||||
FROM golang:$TAG
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install packages
|
||||
RUN apt update && sudo apt upgrade \
|
||||
&& \
|
||||
set -eux; \
|
||||
# Packages to install
|
||||
apt install -y \
|
||||
git \
|
||||
jq \
|
||||
rsync \
|
||||
nodejs \
|
||||
npm \
|
||||
# Needed for headless chrome/tests
|
||||
libglib2.0-dev \
|
||||
libnss3-dev \
|
||||
libdbus-1-dev \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libxcomposite-dev \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libgbm-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libasound2 \
|
||||
&& \
|
||||
# Clean out directories that don't need to be part of the image
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||
&& \
|
||||
# Install node packages \
|
||||
npm install -g npm@^10.0.0 \
|
||||
npm install -g pnpm \
|
||||
&& \
|
||||
# Install flyctl cli \
|
||||
curl -L https://fly.io/install.sh | sh \
|
||||
&& \
|
||||
ln -s /root/.fly/bin/flyctl /usr/local/bin/fly \
|
||||
&& \
|
||||
# Make this a safe .git directory
|
||||
git config --global --add safe.directory /app
|
||||
|
||||
ENTRYPOINT ["/bin/sh"]
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) Delaney Gillilan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,41 @@
|
|||
TAG?=1.24
|
||||
CONTAINER?=$(shell basename $(CURDIR))-dev
|
||||
DEV_PORT?=8080
|
||||
IMAGE_INFO=$(shell docker image inspect $(CONTAINER):$(TAG))
|
||||
IMAGE_NAME=${CONTAINER}:${TAG}
|
||||
DOCKER_RUN=docker container run --rm -it -v "${CURDIR}":/app -v go-modules:/go/pkg/mod
|
||||
ARCH=$(shell uname -m)
|
||||
|
||||
.PHONY: build clean dev image-build task test ssh
|
||||
|
||||
# Perform a dist build
|
||||
build: image-check
|
||||
${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} build
|
||||
# Clean up all build artifacts to start from scratch
|
||||
clean:
|
||||
docker image rm ${IMAGE_NAME}
|
||||
docker volume rm go-modules
|
||||
# Run the development server
|
||||
dev: --image-check
|
||||
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task -w'
|
||||
# Build the Docker image
|
||||
image-build:
|
||||
docker build -f Dockerfile-dev . -t ${IMAGE_NAME} --build-arg TAG=${TAG} --no-cache
|
||||
${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} -c 'go tool task tools'
|
||||
# Run the passed in task command
|
||||
task: --image-check
|
||||
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task $(filter-out $@,$(MAKECMDGOALS)) $(MAKEFLAGS)'
|
||||
# Run the test suite
|
||||
test: --image-check
|
||||
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task test'
|
||||
# Open a shell inside of the container
|
||||
ssh: --image-check
|
||||
${DOCKER_RUN} --name ${CONTAINER}-$@ --entrypoint=/bin/sh ${IMAGE_NAME}
|
||||
# Ensure the image has been created
|
||||
--image-check:
|
||||
ifeq ($(IMAGE_INFO), [])
|
||||
--image-check: image-build
|
||||
endif
|
||||
%:
|
||||
@:
|
||||
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line
|
|
@ -0,0 +1,62 @@
|
|||
[](https://github.com/starfederation/datastar/releases)
|
||||
[](https://github.com/starfederation/datastar/blob/main/LICENSE)
|
||||
[](https://github.com/starfederation/datastar/stargazers)
|
||||
|
||||
<p align="center"><img width="200" src="https://data-star.dev/static/images/rocket.webp"></p>
|
||||
|
||||
# Datastar
|
||||
|
||||
### The hypermedia framework.
|
||||
|
||||
Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.
|
||||
|
||||
Getting started is as easy as adding a single 14.5 KiB script tag to your HTML.
|
||||
|
||||
```html
|
||||
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
|
||||
```
|
||||
|
||||
Then start adding frontend reactivity using declarative <code>data-*</code> attributes.
|
||||
|
||||
```html
|
||||
<input data-bind-title />
|
||||
<div data-text="$title.toUpperCase()"></div>
|
||||
<button data-on-click="@post('/endpoint')">Save</button>
|
||||
```
|
||||
|
||||
Visit the [Datastar Website »](https://data-star.dev/)
|
||||
|
||||
Watch the [Videos »](https://www.youtube.com/@data-star)
|
||||
|
||||
Join the [Discord Server »](https://discord.com/invite/bnRNgZjgPh)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Read the [Getting Started Guide »](https://data-star.dev/guide/getting_started)
|
||||
|
||||
## Contributing
|
||||
|
||||
Read the [Contribution Guidelines »](https://github.com/starfederation/datastar/blob/develop/CONTRIBUTING.md)
|
||||
|
||||
## Custom Plugins
|
||||
|
||||
You can manually add your own plugins to the core:
|
||||
|
||||
```html
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import { load } from 'datastar'
|
||||
|
||||
load(
|
||||
// Look ma’, I made a plugin!
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
[](https://www.star-history.com/#starfederation/datastar&Date)
|
218
Taskfile.yml
218
Taskfile.yml
|
@ -2,99 +2,203 @@
|
|||
|
||||
version: "3"
|
||||
|
||||
interval: 100ms
|
||||
interval: 1000ms
|
||||
|
||||
vars:
|
||||
NAME: "site"
|
||||
BIN_NAME: "{{.NAME}}_bin"
|
||||
VERSION:
|
||||
sh: cat library/package.json| jq -r .version
|
||||
sh: cat VERSION
|
||||
|
||||
tasks:
|
||||
tools:
|
||||
platforms:
|
||||
[
|
||||
windows,
|
||||
linux,
|
||||
darwin/arm64,
|
||||
darwin/amd64,
|
||||
openbsd,
|
||||
dragonfly,
|
||||
freebsd,
|
||||
netbsd,
|
||||
]
|
||||
cmds:
|
||||
- platforms: [linux/amd64]
|
||||
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-x64
|
||||
|
||||
- platforms: [linux/arm64]
|
||||
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-arm64
|
||||
|
||||
- platforms: [darwin/arm64]
|
||||
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-arm64
|
||||
|
||||
- platforms: [darwin/amd64]
|
||||
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-x64
|
||||
|
||||
- platforms: [windows]
|
||||
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-windows-x64.exe
|
||||
|
||||
- platforms: [openbsd, dragonfly, freebsd, netbsd]
|
||||
cmd: pnpm add tailwindcss @tailwindcss/container-queries @tailwindcss/typography daisyui
|
||||
|
||||
- platforms: [openbsd, dragonfly, freebsd, netbsd]
|
||||
cmd: test -f site/tailwindcli || (echo "#!/bin/sh" > site/tailwindcli && echo "tailwindcss $@" >> site/tailwindcli)
|
||||
|
||||
- chmod +x site/tailwindcli
|
||||
|
||||
version:
|
||||
cmds:
|
||||
- echo {{.VERSION}}
|
||||
libpub:
|
||||
dir: ./library
|
||||
|
||||
qtc:
|
||||
sources:
|
||||
- "**/*.qtpl"
|
||||
generates:
|
||||
- "**/*.qtpl.go"
|
||||
cmds:
|
||||
- go tool qtc
|
||||
|
||||
build:
|
||||
deps:
|
||||
- qtc
|
||||
sources:
|
||||
- VERSION
|
||||
- "**/*.qtpl"
|
||||
- "**/package.json"
|
||||
- "**/*.ts"
|
||||
generates:
|
||||
- "bundles/**/*"
|
||||
cmds:
|
||||
- go run build/cmd/build/main.go
|
||||
- cp -r bundles/* site/static/js/
|
||||
- mkdir -p library/dist/
|
||||
- cp -r bundles/* library/dist/
|
||||
- mkdir -p site/static/librarySource
|
||||
- rm -rf site/static/librarySource/*
|
||||
- cp -r library/src/* site/static/librarySource/
|
||||
|
||||
library:
|
||||
dir: library
|
||||
requires:
|
||||
vars: [VERSION]
|
||||
sources:
|
||||
- "**/*.ts"
|
||||
- "**/*.js"
|
||||
- "**/*.json"
|
||||
generates:
|
||||
- "dist/**/*"
|
||||
cmds:
|
||||
- pnpm i
|
||||
- pnpm build
|
||||
|
||||
libpub:
|
||||
dir: library
|
||||
requires:
|
||||
vars: [VERSION]
|
||||
deps:
|
||||
- build
|
||||
- library
|
||||
cmds:
|
||||
- git push origin
|
||||
- git tag v{{.VERSION}}
|
||||
- git push origin --tags
|
||||
- npm publish --access public
|
||||
- git push --tags
|
||||
- GOPROXY=proxy.golang.org go list -m github.com/delaneyj/datastar@v{{.VERSION}}
|
||||
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js
|
||||
- git push origin --tags
|
||||
- GOPROXY=proxy.golang.org go list -m github.com/starfederation/datastar@v{{.VERSION}}
|
||||
- task: deploy
|
||||
|
||||
library:
|
||||
vars:
|
||||
BACKEND_STATIC_DIR: "../backends/go/{{.NAME}}/static/library"
|
||||
dir: library
|
||||
generates:
|
||||
- "dist/**/*"
|
||||
sources:
|
||||
- "src/**/*.ts"
|
||||
- "package.json"
|
||||
sdktspub:
|
||||
dir: sdk/typescript
|
||||
requires:
|
||||
vars: [VERSION]
|
||||
cmds:
|
||||
# - echo "{{.BACKEND_STATIC_DIR}}"
|
||||
- pnpm i
|
||||
# - pnpm vitest --watch=false
|
||||
- pnpm prettier src -w --log-level silent
|
||||
- pnpm build
|
||||
- rsync -av dist/ {{.BACKEND_STATIC_DIR}}
|
||||
- rsync -av package.json {{.BACKEND_STATIC_DIR}}
|
||||
- git push origin
|
||||
- git tag sdktsv{{.VERSION}}
|
||||
- git push origin --tags
|
||||
- deno run -A build.ts {{.VERSION}}
|
||||
- cd npm && npm publish --access public
|
||||
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/web/serverSentEventGenerator.js
|
||||
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/abstractServerSentEventGenerator.js
|
||||
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/node/serverSentEventGenerator.js
|
||||
- git push origin --tags
|
||||
|
||||
css:
|
||||
vars:
|
||||
BACKEND_STATIC_DIR: "../backends/go/{{.NAME}}/static/css"
|
||||
dir: library
|
||||
generates:
|
||||
- "{{.BACKEND_STATIC_DIR}}"
|
||||
sources:
|
||||
- "**/*.md"
|
||||
- "../backends/go/site/**/*.go"
|
||||
idiomorph:
|
||||
dir: library/src/vendored
|
||||
cmds:
|
||||
- pnpm unocss
|
||||
- wget -O idiomorph.esm.js https://raw.githubusercontent.com/bigskysoftware/idiomorph/refs/heads/main/dist/idiomorph.esm.js
|
||||
- npx -p typescript tsc *.js --declaration --allowJs --emitDeclarationOnly --outDir .
|
||||
css:
|
||||
dir: site
|
||||
sources:
|
||||
- "**/*.templ"
|
||||
- "**/*.md"
|
||||
- "**/*.go"
|
||||
generates:
|
||||
- "static/css/site.css"
|
||||
|
||||
cmds:
|
||||
- ./tailwindcli build -i src/css/site.css -o static/css/site.css
|
||||
|
||||
templ:
|
||||
env:
|
||||
TEMPL_EXPERIMENT: rawgo
|
||||
# deps:
|
||||
# - library
|
||||
generates:
|
||||
- "**/*_templ.go"
|
||||
sources:
|
||||
- "**/*.templ"
|
||||
cmds:
|
||||
- go tool templ generate .
|
||||
|
||||
kill:
|
||||
method: none
|
||||
cmds:
|
||||
- killall -q {{.BIN_NAME}} || echo "Process was not running."
|
||||
- fuser -k 8080/tcp > /dev/null 2>&1 || true
|
||||
|
||||
hot:
|
||||
desc: Server hot reload
|
||||
dir: backends/go
|
||||
env:
|
||||
ENV: dev
|
||||
support:
|
||||
sources:
|
||||
- "**/*.templ"
|
||||
- "**/*.go"
|
||||
- "**/*.md"
|
||||
- "{{.NAME}}/static/**/*"
|
||||
- site/static/**/*
|
||||
generates:
|
||||
- ./datastar-website
|
||||
deps:
|
||||
- library
|
||||
- kill
|
||||
# - css
|
||||
- build
|
||||
- templ
|
||||
- css
|
||||
|
||||
site:
|
||||
method: none
|
||||
desc: build and run site
|
||||
deps:
|
||||
- support
|
||||
cmds:
|
||||
- go mod tidy
|
||||
- go build -o ./{{.BIN_NAME}} cmd/{{.NAME}}/main.go
|
||||
- ./{{.BIN_NAME}}
|
||||
|
||||
test:
|
||||
dir: playwright
|
||||
cmds:
|
||||
- curl -o - -I http://localhost:8080
|
||||
- pnpm i
|
||||
- pnpm playwright install
|
||||
- pnpm playwright test
|
||||
- go build -o ./datastar-website "site/cmd/site/main.go"
|
||||
|
||||
deploy:
|
||||
method: none
|
||||
deps:
|
||||
- library
|
||||
- support
|
||||
cmds:
|
||||
- fly deploy --local-only
|
||||
|
||||
default:
|
||||
test:
|
||||
deps:
|
||||
- hot
|
||||
silent: true
|
||||
- support
|
||||
cmds:
|
||||
- go test -v github.com/starfederation/datastar/site/smoketests -run '^TestUnit' -count=1 -timeout=1m
|
||||
|
||||
test-all:
|
||||
deps:
|
||||
- support
|
||||
cmds:
|
||||
- go test -v github.com/starfederation/datastar/site/smoketests -count=1 -timeout=1m
|
||||
|
||||
default:
|
||||
cmds:
|
||||
- task: site
|
||||
- ./datastar-website
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
package site
|
||||
|
||||
import "github.com/goccy/go-json"
|
||||
|
||||
func UnmarshalPackageJSON(data []byte) (PackageJSON, error) {
|
||||
var r PackageJSON
|
||||
err := json.Unmarshal(data, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (r *PackageJSON) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
type PackageJSON struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Repository string `json:"repository"`
|
||||
Files []string `json:"files"`
|
||||
Private bool `json:"private"`
|
||||
Main string `json:"main"`
|
||||
Module string `json:"module"`
|
||||
Types string `json:"types"`
|
||||
DevDependencies map[string]string
|
||||
Dependencies map[string]string
|
||||
}
|
||||
|
||||
var packageJSON PackageJSON
|
|
@ -1,194 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/alecthomas/chroma"
|
||||
"github.com/alecthomas/chroma/formatters/html"
|
||||
"github.com/alecthomas/chroma/lexers"
|
||||
"github.com/alecthomas/chroma/styles"
|
||||
"github.com/benbjohnson/hashfs"
|
||||
"github.com/delaneyj/datastar"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
mdhtml "github.com/gomarkdown/markdown/html"
|
||||
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
var (
|
||||
staticSys = hashfs.NewFS(staticFS)
|
||||
highlightCSS *STYLEElement
|
||||
mdRenderer func() *mdhtml.Renderer
|
||||
)
|
||||
|
||||
func staticPath(path string) string {
|
||||
return "/" + staticSys.HashName("static/"+path)
|
||||
}
|
||||
|
||||
func RunBlocking(port int) toolbelt.CtxErrFunc {
|
||||
upsertIIfeBuildSize()
|
||||
return func(ctx context.Context) error {
|
||||
|
||||
b, err := staticFS.ReadFile("static/library/package.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading package.json: %w", err)
|
||||
}
|
||||
|
||||
packageJSON, err = UnmarshalPackageJSON(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling package.json: %w", err)
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(
|
||||
middleware.Logger,
|
||||
middleware.Recoverer,
|
||||
// toolbelt.CompressMiddleware(),
|
||||
)
|
||||
|
||||
setupRoutes(router)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
srv.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
return srv.ListenAndServe()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func setupRoutes(router chi.Router) error {
|
||||
defer router.Handle("/static/*", hashfs.FileServer(staticSys))
|
||||
defer router.Get("/hotreload", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
<-r.Context().Done()
|
||||
sse.Send("reload", datastar.WithSSERetry(250))
|
||||
})
|
||||
|
||||
htmlFormatter := html.New(html.WithClasses(true), html.TabWidth(2))
|
||||
if htmlFormatter == nil {
|
||||
return fmt.Errorf("couldn't create html formatter")
|
||||
}
|
||||
styleName := "nord"
|
||||
highlightStyle := styles.Get(styleName)
|
||||
if highlightStyle == nil {
|
||||
return fmt.Errorf("couldn't find style %s", styleName)
|
||||
}
|
||||
highlightCSSBuffer := &bytes.Buffer{}
|
||||
if err := htmlFormatter.WriteCSS(highlightCSSBuffer, highlightStyle); err != nil {
|
||||
return fmt.Errorf("error writing highlight css: %w", err)
|
||||
}
|
||||
highlightCSS = STYLE().Text(highlightCSSBuffer.String())
|
||||
|
||||
mdRenderer = func() *mdhtml.Renderer {
|
||||
return mdhtml.NewRenderer(mdhtml.RendererOptions{
|
||||
Flags: mdhtml.CommonFlags | mdhtml.HrefTargetBlank,
|
||||
RenderNodeHook: func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
|
||||
// based on https://github.com/alecthomas/chroma/blob/master/quick/quick.go
|
||||
htmlHighlight := func(w io.Writer, source, lang, defaultLang string) error {
|
||||
if lang == "" {
|
||||
lang = defaultLang
|
||||
}
|
||||
l := lexers.Get(lang)
|
||||
if l == nil {
|
||||
l = lexers.Analyse(source)
|
||||
}
|
||||
if l == nil {
|
||||
l = lexers.Fallback
|
||||
}
|
||||
l = chroma.Coalesce(l)
|
||||
|
||||
it, err := l.Tokenise(nil, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return htmlFormatter.Format(w, highlightStyle, it)
|
||||
}
|
||||
|
||||
renderCode := func(w io.Writer, codeBlock *ast.CodeBlock) {
|
||||
defaultLang := ""
|
||||
lang := string(codeBlock.Info)
|
||||
htmlHighlight(w, string(codeBlock.Literal), lang, defaultLang)
|
||||
}
|
||||
|
||||
if code, ok := node.(*ast.CodeBlock); ok {
|
||||
renderCode(w, code)
|
||||
return ast.GoToNext, true
|
||||
}
|
||||
return ast.GoToNext, false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := errors.Join(
|
||||
setupAPI(router),
|
||||
setupHome(router),
|
||||
setupDocs(router),
|
||||
setupReferenceRoutes(router),
|
||||
setupExamples(router),
|
||||
setupEssays(router),
|
||||
); err != nil {
|
||||
return fmt.Errorf("error setting up routes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buttonLink(isAccent ...bool) *AElement {
|
||||
bg := "bg-primary-600 hover:bg-primary-700"
|
||||
if len(isAccent) > 0 && isAccent[0] {
|
||||
bg = "bg-accent-600 hover:bg-accent-700"
|
||||
}
|
||||
|
||||
return A().
|
||||
CLASS(
|
||||
"font-brand font-bold p-4 cursor-pointer text-primary-50 rounded-md text-center flex gap-2 items-center justify-center",
|
||||
bg,
|
||||
)
|
||||
}
|
||||
|
||||
func link(href, text string, isHighlighted bool) *AElement {
|
||||
return A().
|
||||
IfCLASS(isHighlighted, "text-accent-200 hover:text-accent-100 underline decoration-primary-300").
|
||||
IfCLASS(!isHighlighted, "text-primary-300 hover:text-primary-200 no-underline").
|
||||
CLASS("font-bold").
|
||||
HREF(href).
|
||||
Text(text)
|
||||
}
|
||||
|
||||
func linkChild(href string, children ...ElementRenderer) *AElement {
|
||||
return A(children...).CLASS("hover:bg-accent-500 rounded-full p-2").HREF(href)
|
||||
}
|
||||
|
||||
func datastarLogo() *SVGSVGElement {
|
||||
return SVG_SVG().
|
||||
VIEW_BOX("0 0 128 128").
|
||||
Children(
|
||||
SVG_PATH().
|
||||
FILL("currentColor").
|
||||
D("M124.317 3.683c-.538-.515-1.268-.912-1.897-1.01a27.833 27.833 0 0 0-3.451-.304c-.985-.029-2.582.033-3.564.112-17.149 1.385-33.377 8.61-47.923 17.546-.98.601-2.551 1.604-3.503 2.249-8.577 5.806-16.27 12.957-22.41 21.308-.583.794-1.835 1.339-2.817 1.259-9.968-.814-20.434 2.617-26.808 10.5-.723.893-1.783 2.429-2.407 3.394-3.68 5.69-6.321 12.243-7.146 18.953-.14 1.141.237 3.022 1.255 3.513 1.46.703 3.4.235 5.226-.06 1.132-.184 2.998-.288 4.141-.414l14.352-1.575c.98-.108 1.927.592 2.005 1.574.47 5.897-2.002 11.47-3.318 17.187-.571 2.481 1.301 4.663 4.034 4.034 5.716-1.316 11.288-3.79 17.185-3.32.982.079 1.682 1.026 1.574 2.005l-1.574 14.354c-.125 1.142-.23 3.007-.414 4.14-.295 1.825-.764 3.766-.06 5.225.49 1.018 2.373 1.397 3.514 1.257 6.71-.825 13.263-3.466 18.953-7.146.965-.624 2.5-1.684 3.394-2.407 7.883-6.374 11.312-16.842 10.499-26.81-.08-.982.466-2.232 1.26-2.815 8.351-6.14 15.5-13.835 21.306-22.412.645-.952 1.648-2.524 2.25-3.503 8.935-14.546 16.16-30.774 17.545-47.923.08-.982.141-2.579.112-3.564a27.814 27.814 0 0 0-.303-3.45c-.098-.628-.495-1.36-1.01-1.897zM88.447 31.37c3.95 0 8.38 4.43 8.38 8.38 0 .054-3.604 3.84-8.867 7.502-2.924 2.144-9.243 2.604-8.672 5.944.575 3.37 6.625 1.883 9.938 3.118 5.484 1.898 10.716 4.28 10.718 4.287 1.377 3.703-1.699 9.3-5.456 10.523-.098.032-4.726-2.361-9.744-6.041-2.858-2.079-5.209-7.83-8.282-6.333-3.04 1.48.227 6.917.292 10.62-.17 2.81.169 8.835-.975 11.303-3.162 2.414-9.343 1.286-11.79-1.851-.189-.243.856-5.697 2.729-11.108 1.234-3.443 5.96-7.486 3.605-9.841-2.382-2.382-6.496 2.273-9.939 3.507-5.573 1.93-10.937 2.665-11.205 2.437-2.979-2.54-4.255-8.506-1.852-11.596 3.697-.695 7.772-.911 11.596-.877 3.572.065 8.914 3.316 10.426.292 1.534-3.067-4.223-5.521-6.431-8.38-1.038-1.857-5.834-8.027-5.944-9.84 1.164-3.791 6.912-6.762 10.62-5.36.066.025 2.451 5.791 4.19 10.816 1.235 3.443-.123 9.56 3.216 10.036 3.354.48 3.768-5.781 5.846-8.77 3.701-5.046 7.53-8.768 7.6-8.768z"),
|
||||
SVG_PATH().
|
||||
FILL("currentColor").
|
||||
D("M26.794 83.953C19.57 83.732 15.15 93.67 13.47 99.89c-1.422 5.67-7.77 15.862-4.497 19.135 3.273 3.273 13.464-3.075 19.136-4.497 6.22-1.68 16.157-6.1 15.936-13.322-5.283.223-12.989 5.243-19.165 3.279a2.212 2.212 0 0 1-1.365-1.366c-1.965-6.176 3.054-13.883 3.278-19.166z"),
|
||||
)
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupAPI(router chi.Router) error {
|
||||
|
||||
var globalCount = new(int32)
|
||||
c := int32(toolbelt.Fit(rand.Float32(), 0, 1, -100, 100))
|
||||
globalCount = &c
|
||||
|
||||
type Store struct {
|
||||
Count int32 `json:"count"`
|
||||
}
|
||||
|
||||
globalCountExample := func() ElementRenderer {
|
||||
store := &Store{
|
||||
Count: atomic.LoadInt32(globalCount),
|
||||
}
|
||||
return DIV().
|
||||
ID("global-count-example").
|
||||
CLASS("flex flex-col gap-2").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex gap-2 justify-between items-center").
|
||||
Children(
|
||||
buttonLink(true).
|
||||
CLASS("text-xs p-1").
|
||||
DATASTAR_ON("click", "$count++").
|
||||
Text("Increment Global State +"),
|
||||
buttonLink(true).
|
||||
CLASS("text-xs p-1").
|
||||
DATASTAR_ON("click", "$count--").
|
||||
Text("Decrement Global State -"),
|
||||
DIV().
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
DIV().DATASTAR_TEXT("`Count is ${$count % 2 === 0 ? 'even' : 'odd'}`"),
|
||||
|
||||
INPUT().
|
||||
CLASS("shadow appearance-none border border-accent-500 rounded w-full py-2 px-3 bg-accent-700 text-accent-200 leading-tight focus:outline-none focus:shadow-outline").
|
||||
TYPE("number").
|
||||
NAME("count").
|
||||
CustomData("model", "count"),
|
||||
),
|
||||
),
|
||||
DIV().
|
||||
CLASS("flex gap-4").
|
||||
Children(
|
||||
buttonLink(true).
|
||||
CLASS("flex-1").
|
||||
DATASTAR_ON("click", datastar.GET("/api/globalCount")).
|
||||
Text("Load global count"),
|
||||
buttonLink(true).
|
||||
CLASS("flex-1").
|
||||
DATASTAR_ON("click", datastar.POST("/api/globalCount")).
|
||||
Text("Store global count"),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
router.Route("/api", func(apiRouter chi.Router) {
|
||||
apiRouter.Route("/globalCount", func(globalCountRouter chi.Router) {
|
||||
globalCountRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, globalCountExample())
|
||||
})
|
||||
|
||||
globalCountRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.BodyUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
atomic.StoreInt32(globalCount, store.Count)
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, globalCountExample())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var docNames = []string{
|
||||
"getting_started",
|
||||
"von_deepa",
|
||||
"howl",
|
||||
"batteries_included",
|
||||
"streaming_backend",
|
||||
}
|
||||
|
||||
func setupDocs(router chi.Router) error {
|
||||
mdElementRenderers, mdAnchors, err := markdownRenders("docs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docLabels := lo.Map(docNames, func(name string, i int) string {
|
||||
return strings.ToUpper(strings.ReplaceAll(name, "_", " "))
|
||||
})
|
||||
|
||||
router.Route("/docs", func(docsRouter chi.Router) {
|
||||
docsRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/docs/"+docNames[0], http.StatusFound)
|
||||
})
|
||||
|
||||
sidebarContents := func(r *http.Request) ElementRenderer {
|
||||
return Group(lo.Map(docNames, func(name string, i int) ElementRenderer {
|
||||
return link("/docs/"+name, docLabels[i], strings.HasSuffix(r.URL.Path, name)).CLASS("uppercase font-brand")
|
||||
})...)
|
||||
}
|
||||
|
||||
docsRouter.Get("/{docName}", func(w http.ResponseWriter, r *http.Request) {
|
||||
docName := chi.URLParam(r, "docName")
|
||||
contents, ok := mdElementRenderers[docName]
|
||||
if !ok {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
docIdx := lo.IndexOf(docNames, docName)
|
||||
|
||||
contentGroup := []ElementRenderer{}
|
||||
if docIdx > 0 {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full no-underline").
|
||||
HREF("/docs/"+docNames[docIdx-1]).
|
||||
Text("Back to "+docLabels[docIdx-1]),
|
||||
)
|
||||
}
|
||||
contentGroup = append(contentGroup, contents)
|
||||
if docIdx < len(docNames)-1 {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full no-underline").
|
||||
HREF("/docs/"+docNames[docIdx+1]).
|
||||
Text("Next "+docLabels[docIdx+1]),
|
||||
)
|
||||
}
|
||||
|
||||
prosePage(
|
||||
r,
|
||||
sidebarContents(r),
|
||||
Group(contentGroup...),
|
||||
mdAnchors[docName],
|
||||
).Render(w)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setupEssays(router chi.Router) error {
|
||||
|
||||
mdElementRenderers, mdAnchors, err := markdownRenders("essays")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
essayNames := []string{
|
||||
"why_another_framework",
|
||||
"yes_you_want_a_build_step",
|
||||
"haikus",
|
||||
"grugs_around_fire",
|
||||
"i_am_a_teapot",
|
||||
"event_streams_all_the_way_down",
|
||||
"another_dependency",
|
||||
}
|
||||
|
||||
essayLabels := lo.Map(essayNames, func(name string, i int) string {
|
||||
return strings.ToUpper(strings.ReplaceAll(name, "_", " "))
|
||||
})
|
||||
|
||||
router.Route("/essays", func(essaysRouter chi.Router) {
|
||||
essaysRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/essays/"+essayNames[0], http.StatusFound)
|
||||
})
|
||||
|
||||
sidebarContents := func(r *http.Request) ElementRenderer {
|
||||
return Group(lo.Map(essayNames, func(name string, i int) ElementRenderer {
|
||||
return link("/essays/"+name, essayLabels[i], strings.HasSuffix(r.URL.Path, name)).CLASS("uppercase font-brand")
|
||||
})...)
|
||||
}
|
||||
|
||||
essaysRouter.Get("/{docName}", func(w http.ResponseWriter, r *http.Request) {
|
||||
docName := chi.URLParam(r, "docName")
|
||||
contents, ok := mdElementRenderers[docName]
|
||||
if !ok {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
docIdx := lo.IndexOf(essayNames, docName)
|
||||
|
||||
contentGroup := []ElementRenderer{}
|
||||
if docIdx > 0 {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full").
|
||||
HREF("/essays/"+essayNames[docIdx-1]).
|
||||
Text("Back to "+essayLabels[docIdx-1]).
|
||||
CLASS("flex flex-col justify-center items-center no-underline"))
|
||||
}
|
||||
contentGroup = append(contentGroup, contents)
|
||||
if docIdx < len(essayNames)-1 {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full").
|
||||
HREF("/essays/"+essayNames[docIdx+1]).
|
||||
Text("Next "+essayLabels[docIdx+1]).
|
||||
CLASS("flex flex-col justify-center items-center no-underline"))
|
||||
}
|
||||
|
||||
anchors := mdAnchors[docName]
|
||||
|
||||
prosePage(r, sidebarContents(r), Group(contentGroup...), anchors).Render(w)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,228 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-sanitize/sanitize"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var (
|
||||
sanitizer *sanitize.Sanitizer
|
||||
SignalStore = Group(
|
||||
H4(Text("Signal Store")),
|
||||
PRE().DATASTAR_TEXT("JSON.stringify(ctx.store())"),
|
||||
)
|
||||
)
|
||||
|
||||
func setupExamples(router chi.Router) (err error) {
|
||||
sanitizer, err = sanitize.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating sanitizer: %w", err)
|
||||
}
|
||||
|
||||
mdElementRenderers, _, err := markdownRenders("examples")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Example struct {
|
||||
URL string
|
||||
Label string
|
||||
Description string
|
||||
Prev, Next *Example
|
||||
}
|
||||
type ExampleGroup struct {
|
||||
Label string
|
||||
Examples []*Example
|
||||
}
|
||||
var (
|
||||
prevExample *Example
|
||||
examplesByURL = map[string]*Example{}
|
||||
)
|
||||
examples := lo.Map([]ExampleGroup{
|
||||
{
|
||||
Label: "Backend Examples",
|
||||
Examples: []*Example{
|
||||
{Label: "Node", Description: "example backend in node"},
|
||||
{Label: "Python", Description: "example backend in python"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Ported HTMX Examples",
|
||||
Examples: []*Example{
|
||||
{Label: "Click to Edit", Description: "inline editing of a data object"},
|
||||
{Label: "Bulk Update", Description: "bulk updating of multiple rows of data"},
|
||||
{Label: "Click to Load", Description: "loading data on demand"},
|
||||
{Label: "Delete Row", Description: "row deletion in a table"},
|
||||
{Label: "Edit Row", Description: "how to edit rows in a table"},
|
||||
{Label: "Lazy Load", Description: "how to lazy load content"},
|
||||
{Label: "Fetch Indicator", Description: "show a loading indicator when fetching data"},
|
||||
{Label: "Is Loading Identifier", Description: "use an isLoading set of identifiers in a signal to reflect when an element is fetching"},
|
||||
{Label: "Inline Validation", Description: "how to do inline field validation"},
|
||||
{Label: "Infinite Scroll", Description: "infinite scrolling of a page"},
|
||||
{Label: "Active Search", Description: "the active search box pattern"},
|
||||
{Label: "Progress Bar", Description: "a job-runner like progress bar"},
|
||||
{Label: "Value Select", Description: "making the values of a select dependent on another select"},
|
||||
{Label: "Animations", Description: "various animation techniques"},
|
||||
{Label: "File Upload", Description: "how to upload a file via ajax with a progress bar"},
|
||||
{Label: "Dialogs Browser", Description: "the prompt and confirm dialogs"},
|
||||
{Label: "Lazy Tabs", Description: "how to lazy load tabs"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Web Components Examples",
|
||||
Examples: []*Example{
|
||||
{Label: "Shoelace Kitchensink", Description: "the Shoelace Web Components library"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Reactive Examples",
|
||||
Examples: []*Example{
|
||||
{Label: "Multiline Fragments", Description: "multiline fragments"},
|
||||
{Label: "Scroll Into View", Description: "scrolling an element into view"},
|
||||
{Label: "On Load", Description: "how to load data on page load"},
|
||||
{Label: "Model Binding", Description: "two-way data binding to signals"},
|
||||
{Label: "Disable Button", Description: "how to disable a button while processing"},
|
||||
{Label: "Merge Options", Description: "how to merge options in a select"},
|
||||
{Label: "Redirects", Description: "how to redirect to another page"},
|
||||
{Label: "View Transition API", Description: "using the view transition API"},
|
||||
{Label: "Title Select", Description: "target singletons on the page"},
|
||||
{Label: "Quick Primer Go", Description: "The getting started guide in Go"},
|
||||
},
|
||||
},
|
||||
}, func(g ExampleGroup, i int) ExampleGroup {
|
||||
for j, example := range g.Examples {
|
||||
g.Examples[j].URL = "/examples/" + toolbelt.Cased(g.Examples[j].Label, toolbelt.Snake, toolbelt.Lower)
|
||||
if prevExample != nil {
|
||||
example.Prev = prevExample
|
||||
prevExample.Next = example
|
||||
}
|
||||
prevExample = example
|
||||
examplesByURL[example.URL] = example
|
||||
}
|
||||
return g
|
||||
})
|
||||
|
||||
router.Route("/examples", func(examplesRouter chi.Router) {
|
||||
|
||||
examplesRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, examples[0].Examples[0].URL, http.StatusFound)
|
||||
})
|
||||
|
||||
examplesRouter.Get("/{exampleName}", func(w http.ResponseWriter, r *http.Request) {
|
||||
exampleName := chi.URLParam(r, "exampleName")
|
||||
contents, ok := mdElementRenderers[exampleName]
|
||||
if !ok {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
example, ok := examplesByURL[r.URL.Path]
|
||||
if !ok {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
contentGroup := []ElementRenderer{}
|
||||
if example.Prev != nil {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full").
|
||||
HREF(example.Prev.URL).
|
||||
Text("Back to "+example.Prev.Label).
|
||||
CLASS("flex flex-col justify-center items-center no-underline"))
|
||||
}
|
||||
contentGroup = append(contentGroup, contents)
|
||||
|
||||
nextHREF := "/reference"
|
||||
nextLabel := "Dive deeper"
|
||||
|
||||
if example.Next != nil {
|
||||
nextHREF = example.Next.URL
|
||||
nextLabel = "Next " + example.Next.Label
|
||||
}
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full").
|
||||
HREF(nextHREF).
|
||||
Text(nextLabel).
|
||||
CLASS("flex flex-col justify-center items-center no-underline"))
|
||||
|
||||
sidebarContents := Group(
|
||||
Range(examples, func(g ExampleGroup) ElementRenderer {
|
||||
return DIV(
|
||||
DIV(
|
||||
DIV().CLASS("text-2xl font-bold text-primary").Text(g.Label),
|
||||
HR().CLASS("divider border-primary"),
|
||||
),
|
||||
TABLE().
|
||||
CLASS("table w-full").
|
||||
Children(
|
||||
THEAD(
|
||||
TR(
|
||||
TH().Text("Pattern"),
|
||||
TH().Text("Description"),
|
||||
),
|
||||
),
|
||||
TBODY(
|
||||
Range(g.Examples, func(e *Example) ElementRenderer {
|
||||
return TR().
|
||||
CLASS("hover").
|
||||
Children(
|
||||
TD(link(e.URL, e.Label, e.URL == r.URL.Path)),
|
||||
TD().CLASS("text-xs").Text(e.Description),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
pp := prosePage(r, sidebarContents, Group(contentGroup...), nil)
|
||||
pp.Render(w)
|
||||
})
|
||||
|
||||
if err := errors.Join(
|
||||
setupExamplesClickToEdit(examplesRouter),
|
||||
setupExamplesBulkUpdate(examplesRouter),
|
||||
setupExamplesClickToLoad(examplesRouter),
|
||||
setupExamplesEditRow(examplesRouter),
|
||||
setupExamplesDeleteRow(examplesRouter),
|
||||
setupExamplesLazyLoad(examplesRouter),
|
||||
setupExamplesFetchIndicator(examplesRouter),
|
||||
setupExamplesIsLoadingId(examplesRouter),
|
||||
setupExamplesOnLoad(examplesRouter),
|
||||
setupExamplesDisableButton(examplesRouter),
|
||||
setupExampleInlineValidation(examplesRouter),
|
||||
setupExamplesInfiniteScroll(examplesRouter),
|
||||
setupExamplesActiveSearch(examplesRouter),
|
||||
setupExamplesProgressBar(examplesRouter),
|
||||
setupExamplesValueSelect(examplesRouter),
|
||||
setupExamplesAnimations(examplesRouter),
|
||||
setupExamplesFileUpload(examplesRouter),
|
||||
setupExamplesDialogsBrowser(examplesRouter),
|
||||
setupExamplesLazyTabs(examplesRouter),
|
||||
setupExamplesMergeOptions(examplesRouter),
|
||||
setupExamplesRedirects(examplesRouter),
|
||||
setupExamplesMultilineFragments(examplesRouter),
|
||||
//
|
||||
setupExamplesShoelaceKitchensink(examplesRouter),
|
||||
//
|
||||
setupExamplesViewTransitionAPI(examplesRouter),
|
||||
setupExamplesModelBinding(examplesRouter),
|
||||
setupExamplesTitleSelect(examplesRouter),
|
||||
setupExamplesScrollIntoView(examplesRouter),
|
||||
setupExamplesQuickPrimerGo(examplesRouter),
|
||||
); err != nil {
|
||||
panic(fmt.Sprintf("error setting up examples routes: %s", err))
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-faker/faker/v4"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
)
|
||||
|
||||
func setupExamplesActiveSearch(examplesRouter chi.Router) error {
|
||||
|
||||
// activeSearchRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// examplePage(w, r)
|
||||
// })
|
||||
|
||||
type Store struct {
|
||||
Search string `json:"search"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
users := make([]*User, 256)
|
||||
for i := range users {
|
||||
u := &User{
|
||||
ID: faker.UUIDHyphenated(),
|
||||
FirstName: faker.FirstName(),
|
||||
LastName: faker.LastName(),
|
||||
Email: faker.Email(),
|
||||
}
|
||||
users[i] = u
|
||||
}
|
||||
|
||||
examplesRouter.Get("/active_search/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
scores := map[string]float64{}
|
||||
s := strings.ToLower(store.Search)
|
||||
minScore, maxScore := math.MaxFloat64, -math.MaxFloat64
|
||||
for _, u := range users {
|
||||
fn := strings.ToLower(u.FirstName)
|
||||
ln := strings.ToLower(u.LastName)
|
||||
e := strings.ToLower(u.Email)
|
||||
|
||||
fnd := fuzzy.LevenshteinDistance(s, fn)
|
||||
lnd := fuzzy.LevenshteinDistance(s, ln)
|
||||
ed := fuzzy.LevenshteinDistance(s, e)
|
||||
|
||||
dist := float64(fnd + lnd + ed)
|
||||
scores[u.ID] = dist
|
||||
|
||||
minScore = min(minScore, dist)
|
||||
maxScore = max(maxScore, dist)
|
||||
}
|
||||
|
||||
for id, score := range scores {
|
||||
scores[id] = toolbelt.Fit(score, minScore, maxScore, 100, 0)
|
||||
}
|
||||
|
||||
// copy users
|
||||
filteredUsers := make([]*User, len(users))
|
||||
copy(filteredUsers, users)
|
||||
|
||||
// sort users by score
|
||||
slices.SortFunc(filteredUsers, func(a, b *User) int {
|
||||
return int(10000 * (scores[b.ID] - scores[a.ID]))
|
||||
})
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("active_search").
|
||||
CLASS("flex flex-col gap-4").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex gap-2").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex-1").
|
||||
Text("Search Contacts"),
|
||||
),
|
||||
DIV().
|
||||
CLASS("form-control").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex gap-2").
|
||||
Children(
|
||||
INPUT().
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
|
||||
TYPE("text").
|
||||
PLACEHOLDER("Search...").
|
||||
DATASTAR_MODEL("search").
|
||||
DATASTAR_ON("input", datastar.GET("/examples/active_search/data"), InputOnModDebounce(1*time.Second)),
|
||||
svg_spinners.BlocksWave().
|
||||
CLASS("text-5xl datastar-indicator"),
|
||||
),
|
||||
),
|
||||
TABLE().
|
||||
CLASS("table w-full").
|
||||
Children(
|
||||
CAPTION(Text("Contacts")),
|
||||
THEAD(
|
||||
TR(
|
||||
TH(Text("First Name")),
|
||||
TH(Text("Last Name")),
|
||||
TH(Text("Email")),
|
||||
TH(Text("Score")),
|
||||
),
|
||||
),
|
||||
TBODY(
|
||||
Range(filteredUsers[0:10], func(u *User) ElementRenderer {
|
||||
score := scores[u.ID]
|
||||
return TR(
|
||||
TD(Text(u.FirstName)),
|
||||
TD(Text(u.LastName)),
|
||||
TD(Text(u.Email)),
|
||||
TD(TextF("%0.2f", score)),
|
||||
)
|
||||
}),
|
||||
).ID("active_search_rows"),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesAnimations(examplesRouter chi.Router) error {
|
||||
// lazyLoadRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// examplePage(w, r)
|
||||
// })
|
||||
|
||||
type Color struct {
|
||||
Label string `json:"label"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
fgPal := []Color{
|
||||
{Label: "red", Value: 0xfb4934},
|
||||
{Label: "green", Value: 0xb8bb26},
|
||||
{Label: "yellow", Value: 0xfabd2f},
|
||||
{Label: "blue", Value: 0x83a598},
|
||||
{Label: "purple", Value: 0xd3869b},
|
||||
{Label: "aqua", Value: 0x8ec07c},
|
||||
{Label: "orange", Value: 0xfe8019},
|
||||
}
|
||||
|
||||
bgPal := []Color{
|
||||
{Label: "red", Value: 0x9d0006},
|
||||
{Label: "green", Value: 0x79740e},
|
||||
{Label: "yellow", Value: 0xb57614},
|
||||
{Label: "blue", Value: 0x458588},
|
||||
{Label: "purple", Value: 0x8f3f71},
|
||||
{Label: "aqua", Value: 0x427b58},
|
||||
{Label: "orange", Value: 0xaf3a03},
|
||||
}
|
||||
|
||||
type RestoreStore struct {
|
||||
ShouldRestore bool `json:"shouldRestore"`
|
||||
}
|
||||
|
||||
renderViewTransition := func(sse *datastar.ServerSentEventsHandler, store *RestoreStore) {
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("view_transition").
|
||||
DATASTAR_STORE(store).
|
||||
CLASS("slide-it").
|
||||
Children(
|
||||
BUTTON().
|
||||
CLASS("btn btn-primary").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/animations/data/view_transition")).
|
||||
Children(
|
||||
Tern(store.ShouldRestore, material_symbols.ArrowLeft(), material_symbols.ArrowRight()),
|
||||
Tern(store.ShouldRestore, Text("Restore It!"), Text("Swap It!")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
examplesRouter.Route("/animations/data", func(dataRouter chi.Router) {
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
BUTTON().
|
||||
ID("fade_out_swap").
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-error-700 hover:bg-error-600").
|
||||
DATASTAR_ON("click", datastar.DELETE("/examples/animations/data")).
|
||||
Children(
|
||||
material_symbols.Delete(),
|
||||
Text("Fade out then delete on click"),
|
||||
),
|
||||
)
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
BUTTON().
|
||||
ID("fade_me_in").
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/animations/data/fade_me_in")).
|
||||
Children(
|
||||
material_symbols.Add(),
|
||||
Text("Fade me in on click"),
|
||||
),
|
||||
)
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("request_in_flight").
|
||||
CLASS("flex flex-col gap-4").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("form-control").
|
||||
Children(
|
||||
LABEL(Text("Name")).CLASS("label label-text"),
|
||||
DIV().
|
||||
CLASS("flex gap-2 items-center").
|
||||
Children(
|
||||
INPUT().
|
||||
TYPE("text").
|
||||
NAME("name").
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5"),
|
||||
DIV().
|
||||
ID("request_in_flight_indicator").
|
||||
Children(
|
||||
svg_spinners.BlocksWave().CLASS("text-xl"),
|
||||
),
|
||||
),
|
||||
),
|
||||
BUTTON().
|
||||
ID("submit_request_in_flight").
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
|
||||
DATASTAR_ON("click", datastar.POST("/examples/animations/data/request_in_flight")).
|
||||
DATASTAR_FETCH_INDICATOR("'#request_in_flight_indicator'").
|
||||
Children(
|
||||
material_symbols.PersonAdd(),
|
||||
Text("Submit"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
renderViewTransition(sse, &RestoreStore{ShouldRestore: false})
|
||||
|
||||
colorThrobTicker := time.NewTicker(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-colorThrobTicker.C:
|
||||
fg := fgPal[rand.Intn(len(fgPal))]
|
||||
bg := bgPal[rand.Intn(len(bgPal))]
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("color_throb").
|
||||
CustomData("testid", "color_throb").
|
||||
CLASS("transition-all duration-1000 font-bold text-2xl text-center rounded-box p-4 uppercase").
|
||||
STYLEF("color", "#%x", fg.Value).
|
||||
STYLEF("background-color", "#%x", bg.Value).
|
||||
TextF("%s on %s", fg.Label, bg.Label),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dataRouter.Delete("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
BUTTON().
|
||||
ID("fade_out_swap").
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-error-700 hover:bg-error-600 transition-all duration-[2000ms] opacity-0").
|
||||
DATASTAR_ON("click", datastar.DELETE("/examples/animations/data")).
|
||||
Children(
|
||||
material_symbols.Delete(),
|
||||
Text("Fade out then delete on click"),
|
||||
),
|
||||
)
|
||||
time.Sleep(2 * time.Second)
|
||||
datastar.Delete(sse, "#fade_out_swap")
|
||||
})
|
||||
|
||||
dataRouter.Get("/fade_me_in", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
BUTTON().
|
||||
ID("fade_me_in").
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600 opacity-0").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/animations/data/fade_me_in")).
|
||||
Children(
|
||||
material_symbols.Add(),
|
||||
Text("Fade me in on click"),
|
||||
),
|
||||
)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
BUTTON().
|
||||
ID("fade_me_in").
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600 transition-all duration-1000 opacity-100").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/animations/data/fade_me_in")).
|
||||
Children(
|
||||
material_symbols.Add(),
|
||||
Text("Fade me in on click"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
dataRouter.Post("/request_in_flight", func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
datastar.RenderFragment(
|
||||
datastar.NewSSE(w, r),
|
||||
DIV().
|
||||
ID("request_in_flight").
|
||||
CLASS("flex gap-2").
|
||||
Text("Submitted!"),
|
||||
)
|
||||
})
|
||||
|
||||
dataRouter.Get("/view_transition", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &RestoreStore{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
store.ShouldRestore = !store.ShouldRestore
|
||||
sse := datastar.NewSSE(w, r)
|
||||
renderViewTransition(sse, store)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type ContactActive struct {
|
||||
ID int `json:"id"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func starterActiveContacts() []*ContactActive {
|
||||
return []*ContactActive{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "Joe Smith",
|
||||
Email: "joe@smith.org",
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: "Angie MacDowell",
|
||||
Email: "angie@macdowell.org",
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: "Fuqua Tarkenton",
|
||||
Email: "fuqua@tarkenton.org",
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Name: "Kim Yee",
|
||||
Email: "kim@yee.org",
|
||||
IsActive: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setupExamplesBulkUpdate(examplesRouter chi.Router) error {
|
||||
contactToNode := func(i int, cs *ContactActive, wasChanged bool) ElementRenderer {
|
||||
key := fmt.Sprintf("contact_%d", i)
|
||||
return TR().
|
||||
ID(key).
|
||||
IfCLASS(i%2 == 0, "bg-accent-800").
|
||||
IfCLASS(i%2 == 1, "bg-accent-700").
|
||||
IfCLASS(wasChanged && cs.IsActive, "activate").
|
||||
IfCLASS(wasChanged && !cs.IsActive, "deactivate").
|
||||
Children(
|
||||
TD(
|
||||
INPUT().CLASS("checkbox").TYPE("checkbox").DATASTAR_MODEL("selections."+key),
|
||||
TD(Text(cs.Name)),
|
||||
TD(Text(cs.Email)),
|
||||
TD(Tern(cs.IsActive, Text("Active"), Text("Inactive"))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
contacts := starterActiveContacts()
|
||||
|
||||
type SelectionStore struct {
|
||||
Selections map[string]bool `json:"selections"`
|
||||
}
|
||||
|
||||
defaultSelectionStore := func() SelectionStore {
|
||||
selections := map[string]bool{
|
||||
"all": false,
|
||||
}
|
||||
for i := range contacts {
|
||||
key := fmt.Sprintf("contact_%d", i)
|
||||
selections[key] = false
|
||||
}
|
||||
return SelectionStore{
|
||||
Selections: selections,
|
||||
}
|
||||
}
|
||||
|
||||
activateButtonCSS := "flex gap-2 items-center px-4 py-2 bg-success-500 hover:bg-success-600 text-success-100 font-bold rounded-lg"
|
||||
deactivateButtonCSS := "flex gap-2 items-center px-4 py-2 bg-error-500 hover:bg-error-600 text-error-100 font-bold rounded-lg"
|
||||
|
||||
contactsToNode := func(selectionStore SelectionStore, contacts []*ContactActive) ElementRenderer {
|
||||
return DIV().
|
||||
ID("bulk_update").
|
||||
DATASTAR_STORE(selectionStore).
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
TABLE().
|
||||
Children(
|
||||
CAPTION().CLASS("text-sm text-accent-300").Text("Select Rows And Activate Or Deactivate Below"),
|
||||
THEAD(
|
||||
TR(
|
||||
TH(
|
||||
INPUT().
|
||||
CLASS("checkbox").
|
||||
TYPE("checkbox").
|
||||
DATASTAR_MODEL("selections.all").
|
||||
DATASTAR_ON("change", "$$setAll('contact_', $selections.all)"),
|
||||
),
|
||||
TH(Text("Name")),
|
||||
TH(Text("Email")),
|
||||
TH(Text("Status")),
|
||||
),
|
||||
),
|
||||
TBODY(
|
||||
RangeI(contacts, func(i int, cs *ContactActive) ElementRenderer {
|
||||
return contactToNode(i, cs, false)
|
||||
}),
|
||||
),
|
||||
),
|
||||
DIV().
|
||||
CLASS("flex gap-2").
|
||||
Children(
|
||||
BUTTON(
|
||||
material_symbols.AccountCircle(),
|
||||
Text("Activate"),
|
||||
).
|
||||
DATASTAR_ON("click", "$$put('/examples/bulk_update/data/activate'); $selections.all = false; $$setAll('contact_', $selections.all)").
|
||||
CLASS(activateButtonCSS),
|
||||
BUTTON(
|
||||
material_symbols.AccountCircleOff(),
|
||||
Text("Deactivate"),
|
||||
).
|
||||
DATASTAR_ON("click", "$$put('/examples/bulk_update/data/deactivate'); $selections.all = false; $$setAll('contact_', $selections.all)").
|
||||
CLASS(deactivateButtonCSS),
|
||||
),
|
||||
SignalStore,
|
||||
)
|
||||
}
|
||||
|
||||
examplesRouter.Route("/bulk_update/data", func(dataRouter chi.Router) {
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragmentSelf(sse, contactsToNode(defaultSelectionStore(), contacts))
|
||||
})
|
||||
|
||||
setActivation := func(w http.ResponseWriter, r *http.Request, isActive bool) {
|
||||
store := &SelectionStore{}
|
||||
if err := datastar.BodyUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
for key, wasSelected := range store.Selections {
|
||||
const prefix = "contact_"
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
idStr := strings.TrimPrefix(key, prefix)
|
||||
i, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
c := contacts[i]
|
||||
wasChanged := c.IsActive != isActive
|
||||
if wasSelected {
|
||||
if wasChanged {
|
||||
c.IsActive = isActive
|
||||
}
|
||||
}
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
contactToNode(i, c, wasChanged && wasSelected),
|
||||
// datastar.WithSettleDuration(5*time.Second),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for k := range store.Selections {
|
||||
store.Selections[k] = false
|
||||
}
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().DATASTAR_STORE(store),
|
||||
// datastar.WithQuerySelector("#bulk_update"),
|
||||
datastar.WithMergeType(datastar.FragmentMergeUpsertAttributes),
|
||||
)
|
||||
}
|
||||
|
||||
dataRouter.Put("/activate", func(w http.ResponseWriter, r *http.Request) {
|
||||
setActivation(w, r, true)
|
||||
})
|
||||
|
||||
dataRouter.Put("/deactivate", func(w http.ResponseWriter, r *http.Request) {
|
||||
setActivation(w, r, false)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
goaway "github.com/TwiN/go-away"
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesClickToEdit(examplesRouter chi.Router) error {
|
||||
|
||||
// tsBytes, err := staticFS.ReadFile("static/examples/click_to_edit.txt")
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("error reading examples dir: %w", err)
|
||||
// }
|
||||
|
||||
// examplesRouter.Route("/click_to_edit", func(exampleRouter chi.Router) {
|
||||
type Contact struct {
|
||||
FirstName string `json:"firstName,omitempty" san:"trim,xss,max=128"`
|
||||
LastName string `json:"lastName,omitempty" san:"trim,xss,max=128"`
|
||||
Email string `json:"email,omitempty" san:"trim,xss,max=128"`
|
||||
}
|
||||
|
||||
// exampleRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// examplePage(w, r)
|
||||
// })
|
||||
|
||||
leftBtnCSS := "bg-primary-300 hover:bg-primary-400 text-primary-800 font-bold py-2 px-4 rounded-l"
|
||||
rightBtnCSS := "bg-accent-300 hover:bg-accent-400 text-accent-800 font-bold py-2 px-4 rounded-r"
|
||||
|
||||
contactNode := func(c *Contact) ElementRenderer {
|
||||
return DIV().
|
||||
ID("contact_1").
|
||||
CLASS("flex flex-col gap-2 max-w-sm").
|
||||
Children(
|
||||
LABEL(TextF("First Name: %s", goaway.Censor(c.FirstName))),
|
||||
LABEL(TextF("Last Name: %s", goaway.Censor(c.LastName))),
|
||||
LABEL(TextF("Email: %s", goaway.Censor(c.Email))),
|
||||
DIV().
|
||||
CLASS("join").
|
||||
Children(
|
||||
BUTTON().
|
||||
CLASS(leftBtnCSS).
|
||||
DATASTAR_ON("click", datastar.GET("/examples/click_to_edit/contact/1/edit")).
|
||||
Text("Edit"),
|
||||
BUTTON().
|
||||
CLASS(rightBtnCSS).
|
||||
DATASTAR_ON("click", datastar.PATCH("/examples/click_to_edit/contact/1/reset")).
|
||||
Text("Reset"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
c1 := &Contact{}
|
||||
resetContact := func() {
|
||||
c1.FirstName = "John"
|
||||
c1.LastName = "Doe"
|
||||
c1.Email = "joe@blow.com"
|
||||
}
|
||||
resetContact()
|
||||
|
||||
examplesRouter.Route("/click_to_edit/contact/{id}", func(contactRouter chi.Router) {
|
||||
contactRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, contactNode(c1))
|
||||
})
|
||||
|
||||
labeledInput := func(label, id string) (container ElementRenderer, input *INPUTElement) {
|
||||
input = INPUT().
|
||||
ID(id).
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5")
|
||||
|
||||
container = DIV(
|
||||
LABEL().FOR(id).CLASS("block mb-2 text-sm font-medium text-accent-100").Text(label),
|
||||
input,
|
||||
)
|
||||
return container, input
|
||||
}
|
||||
|
||||
labelInputModel := func(label, id string) ElementRenderer {
|
||||
container, input := labeledInput(label, id)
|
||||
input.TYPE("text").DATASTAR_MODEL(id)
|
||||
return container
|
||||
}
|
||||
|
||||
contactRouter.Get("/edit", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("contact_1").
|
||||
CLASS("flex flex-col gap-2").
|
||||
DATASTAR_STORE(c1).
|
||||
Children(
|
||||
labelInputModel("First Name", "firstName"),
|
||||
labelInputModel("Last Name", "lastName"),
|
||||
labelInputModel("Email", "email"),
|
||||
DIV().
|
||||
CLASS("inline-flex").
|
||||
Children(
|
||||
BUTTON().
|
||||
CLASS(leftBtnCSS).
|
||||
DATASTAR_ON("click", datastar.PUT("/examples/click_to_edit/contact/1")).
|
||||
Text("Save"),
|
||||
BUTTON().
|
||||
CLASS(rightBtnCSS).
|
||||
DATASTAR_ON("click", datastar.GET("/examples/click_to_edit/contact/1")).
|
||||
Text("Cancel"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
contactRouter.Patch("/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
resetContact()
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, contactNode(c1))
|
||||
})
|
||||
|
||||
contactRouter.Put("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
c := &Contact{}
|
||||
if err := datastar.BodyUnmarshal(r, c); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.Print(c)
|
||||
|
||||
if err := sanitizer.Sanitize(c); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
c1 = c // update the contact
|
||||
datastar.RenderFragment(datastar.NewSSE(w, r), contactNode(c1))
|
||||
})
|
||||
})
|
||||
// })
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesClickToLoad(examplesRouter chi.Router) error {
|
||||
|
||||
renderAgentRow := func(i int) ElementRenderer {
|
||||
return TR().
|
||||
ID(fmt.Sprintf("agent_%d", i)).
|
||||
Children(
|
||||
TD().Text("Agent Smith"),
|
||||
TD().TextF("void%d@null.org", i+1),
|
||||
TD().CLASS("uppercase").TextF("%x", toolbelt.AliasHash(fmt.Sprint(i))),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
SidebarOpen bool `json:"sidebarOpen"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
renderAgentRows := func(input *Input) []ElementRenderer {
|
||||
arr := make([]ElementRenderer, input.Limit)
|
||||
for i := range arr {
|
||||
arr[i] = renderAgentRow(i + input.Offset)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
moreButton := func(input *Input) ElementRenderer {
|
||||
expression := fmt.Sprintf(
|
||||
"$offset=%d; $limit=%d; %s",
|
||||
input.Offset+input.Limit,
|
||||
input.Limit,
|
||||
datastar.GET("/examples/click_to_load/data"),
|
||||
)
|
||||
return BUTTON().
|
||||
ID("more_btn").
|
||||
CLASS("btn btn-primary").
|
||||
DATASTAR_ON("click", expression).
|
||||
Text("Load More")
|
||||
}
|
||||
|
||||
renderAgentsTable := func(input *Input) ElementRenderer {
|
||||
arr := make([]int, input.Limit)
|
||||
for i := range arr {
|
||||
arr[i] = i + input.Offset
|
||||
}
|
||||
|
||||
return DIV().
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
TABLE().
|
||||
Children(
|
||||
CAPTION(Text("Agents")),
|
||||
THEAD(
|
||||
TR(
|
||||
TH(Text("Name")),
|
||||
TH(Text("Email")),
|
||||
TH(Text("ID")),
|
||||
),
|
||||
),
|
||||
TBODY().
|
||||
ID("click_to_load_rows").
|
||||
DATASTAR_STORE(input).
|
||||
Children(
|
||||
Group(renderAgentRows(input)...),
|
||||
),
|
||||
),
|
||||
moreButton(input),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// clickToLoadRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// examplePage(w, r)
|
||||
// })
|
||||
|
||||
examplesRouter.Get("/click_to_load/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
input := &Input{}
|
||||
if err := datastar.QueryStringUnmarshal(r, input); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
if input.Limit < 1 {
|
||||
input.Limit = 10
|
||||
} else if input.Limit > 100 {
|
||||
input.Limit = 100
|
||||
}
|
||||
if input.Offset < 0 {
|
||||
input.Offset = 0
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
if input.Offset == 0 {
|
||||
datastar.RenderFragmentSelf(sse, renderAgentsTable(input))
|
||||
} else {
|
||||
datastar.RenderFragment(sse, moreButton(input))
|
||||
for _, node := range renderAgentRows(input) {
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
node,
|
||||
datastar.WithQuerySelectorID("click_to_load_rows"),
|
||||
datastar.WithMergeAppendElement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setupExamplesDeleteRow(examplesRouter chi.Router) error {
|
||||
|
||||
contacts := starterActiveContacts()
|
||||
|
||||
contactNode := func(i int, cs *ContactActive) ElementRenderer {
|
||||
return TR().
|
||||
ID(fmt.Sprintf("contact_%d", cs.ID)).
|
||||
Children(
|
||||
TD(Text(cs.Name)),
|
||||
TD(Text(cs.Email)),
|
||||
TD(Tern(cs.IsActive, Text("Active"), Text("Inactive"))),
|
||||
TD().
|
||||
CLASS("flex justify-end").
|
||||
Children(
|
||||
BUTTON().
|
||||
CLASS("flex gap-2 items-center px-4 py-2 bg-error-600 hover:bg-error-500 rounded-lg").
|
||||
DATASTAR_ON("click", fmt.Sprintf(
|
||||
`confirm('Are you sure?') && %s`,
|
||||
datastar.DELETE("/examples/delete_row/data/%d", cs.ID),
|
||||
)).
|
||||
Children(
|
||||
material_symbols.Delete(),
|
||||
Text("Delete"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
contactsToNode := func() ElementRenderer {
|
||||
return DIV().
|
||||
ID("contacts").
|
||||
CLASS("flex flex-col gap-8").
|
||||
Children(
|
||||
TABLE().
|
||||
CLASS("table w-full").
|
||||
Children(
|
||||
CAPTION(Text("Contacts")),
|
||||
THEAD(
|
||||
TR(
|
||||
TH(Text("Name")),
|
||||
TH(Text("Email")),
|
||||
TH(Text("Status")),
|
||||
TH(Text("Actions")).CLASS("text-right")),
|
||||
),
|
||||
TBODY(
|
||||
RangeI(contacts, contactNode),
|
||||
),
|
||||
),
|
||||
DIV(
|
||||
BUTTON().
|
||||
CLASS("flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-400 hover:bg-primary-500").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/delete_row/data/reset")).
|
||||
Children(
|
||||
material_symbols.Refresh(),
|
||||
Text("Reset"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
examplesRouter.Route("/delete_row/data", func(dataRouter chi.Router) {
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragmentSelf(sse, contactsToNode())
|
||||
})
|
||||
|
||||
dataRouter.Get("/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
contacts = starterActiveContacts()
|
||||
datastar.RenderFragment(sse, contactsToNode())
|
||||
})
|
||||
|
||||
dataRouter.Delete("/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error parsing index: %s", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
contacts = lo.Filter(contacts, func(cs *ContactActive, i int) bool {
|
||||
return cs.ID != id
|
||||
})
|
||||
datastar.Delete(sse, "#contact_"+idStr)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesDialogsBrowser(examplesRouter chi.Router) error {
|
||||
type Store struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Confirm bool `json:"confirm"`
|
||||
}
|
||||
|
||||
examplesRouter.Get("/dialogs_browser/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
BUTTON().
|
||||
ID("dialogs").
|
||||
CLASS("flex items-center justify-center gap-1 px-2 py-1 rounded-sm text-xs bg-primary-600 hover:bg-primary-500").
|
||||
DATASTAR_STORE(&Store{Prompt: "foo"}).
|
||||
DATASTAR_ON("click", `$prompt = prompt('Enter a string',$prompt);$confirm = confirm('Are you sure?');$confirm && $$get('/examples/dialogs_browser/sure')`).
|
||||
Children(
|
||||
Text("Click Me"),
|
||||
material_symbols.QuestionMark(),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
examplesRouter.Get("/dialogs_browser/sure", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("dialogs").
|
||||
CLASS("flex flex-col gap-4").
|
||||
Children(
|
||||
Tern(
|
||||
store.Confirm,
|
||||
Group(
|
||||
DIV(
|
||||
TextF("You clicked the button and confirmed with prompt of "),
|
||||
SPAN().CLASS("font-bold text-accent").Text(store.Prompt),
|
||||
Text("!"),
|
||||
),
|
||||
BUTTON().
|
||||
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-accent-600 hover:bg-accent-500").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/dialogs_browser/data")).
|
||||
Children(
|
||||
material_symbols.ArrowBack(),
|
||||
Text("Reset"),
|
||||
),
|
||||
),
|
||||
DIV().
|
||||
CLASS("flex gap-2 items-center justify-between font-regular relative mb-4 block w-full rounded-lg bg-red-500 p-4 text-base leading-5 text-white opacity-100").
|
||||
Children(
|
||||
material_symbols.ErrorIcon(),
|
||||
Text("You clicked the button and did not confirm! Should not see this"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesDisableButton(examplesRouter chi.Router) error {
|
||||
examplesRouter.Get("/disable_button/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
store := map[string]any{
|
||||
"shouldDisable": false,
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
datastar.RenderFragment(sse,
|
||||
DIV().TextF("The time is %s", time.Now().UTC().Format(time.RFC3339)),
|
||||
datastar.WithQuerySelectorID("results"),
|
||||
datastar.WithMergeAppendElement(),
|
||||
)
|
||||
datastar.RenderFragment(sse, DIV().ID("container").DATASTAR_STORE(store), datastar.WithMergeUpsertAttributes())
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type ContactEdit struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func starterEditContacts() []*ContactEdit {
|
||||
return []*ContactEdit{
|
||||
{
|
||||
Name: "Joe Smith",
|
||||
Email: "joe@smith.org",
|
||||
},
|
||||
{
|
||||
Name: "Angie MacDowell",
|
||||
Email: "angie@macdowell.org",
|
||||
},
|
||||
{
|
||||
Name: "Fuqua Tarkenton",
|
||||
Email: "fuqua@tarkenton.org",
|
||||
},
|
||||
{
|
||||
Name: "Kim Yee",
|
||||
Email: "kim@yee.org",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setupExamplesEditRow(examplesRouter chi.Router) error {
|
||||
contacts := starterEditContacts()
|
||||
|
||||
type Store struct {
|
||||
EditRowIndex int `json:"editRowIndex"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
contactNode := func(i int, isEditingRow, isEditingAnyRow bool) ElementRenderer {
|
||||
contact := contacts[i]
|
||||
contactKeyPrefix := fmt.Sprintf("contact_%d", i)
|
||||
return TR().
|
||||
ID(contactKeyPrefix).
|
||||
Children(
|
||||
TD(Tern(
|
||||
isEditingRow,
|
||||
INPUT().
|
||||
TYPE("text").
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
|
||||
DATASTAR_MODEL("name").
|
||||
CustomData("testid", contactKeyPrefix+"_name"),
|
||||
DIV().Text(contact.Name),
|
||||
)),
|
||||
TD(Tern(
|
||||
isEditingRow,
|
||||
INPUT().
|
||||
TYPE("text").
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
|
||||
DATASTAR_MODEL("email").
|
||||
CustomData("testid", contactKeyPrefix+"_email"),
|
||||
DIV().Text(contact.Email),
|
||||
)),
|
||||
TD().
|
||||
CLASS("flex justify-end").
|
||||
Children(
|
||||
DynTern(
|
||||
isEditingAnyRow,
|
||||
func() ElementRenderer {
|
||||
return DIV().
|
||||
CLASS("flex gap-2").
|
||||
Children(
|
||||
BUTTON().
|
||||
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-primary-600 hover:bg-primary-500").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/edit_row/data")).
|
||||
CustomData("testid", contactKeyPrefix+"_cancel").
|
||||
Children(
|
||||
material_symbols.Cancel(),
|
||||
Text("Cancel"),
|
||||
),
|
||||
BUTTON().
|
||||
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-success-600 hover:bg-success-500").
|
||||
DATASTAR_ON("click", datastar.PATCH("/examples/edit_row/edit")).
|
||||
CustomData("testid", contactKeyPrefix+"_save").
|
||||
Children(
|
||||
material_symbols.Save(),
|
||||
Text("Save"),
|
||||
),
|
||||
)
|
||||
},
|
||||
func() ElementRenderer {
|
||||
return BUTTON().
|
||||
CLASS("flex items-center gap-1 px-2 py-1 rounded-sm text-xs bg-accent-600 hover:bg-accent-500").
|
||||
DATASTAR_ON("click", fmt.Sprintf(
|
||||
"$editRowIndex = %d; %s", i,
|
||||
datastar.GET("/examples/edit_row/edit"),
|
||||
)).
|
||||
CustomData("testid", contactKeyPrefix+"_edit").
|
||||
Children(
|
||||
material_symbols.Edit(),
|
||||
Text("Edit"),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
contactsToNode := func(store *Store) ElementRenderer {
|
||||
return DIV().
|
||||
ID("edit_row").
|
||||
CLASS("flex flex-col").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
TABLE().
|
||||
CLASS("table w-full").
|
||||
Children(
|
||||
CAPTION(Text("Contacts")),
|
||||
THEAD(
|
||||
TR(
|
||||
TH(Text("Name")),
|
||||
TH(Text("Email")),
|
||||
TH(Text("Actions")).CLASS("text-right"),
|
||||
),
|
||||
),
|
||||
TBODY().
|
||||
ID("edit_row_table_body").
|
||||
Children(
|
||||
RangeI(contacts, func(i int, cs *ContactEdit) ElementRenderer {
|
||||
log.Print(cs)
|
||||
return contactNode(i, i == store.EditRowIndex, store.EditRowIndex != -1)
|
||||
}),
|
||||
),
|
||||
),
|
||||
DIV(
|
||||
BUTTON().
|
||||
CLASS("flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-600 hover:bg-primary-500").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/edit_row/reset")).
|
||||
CustomData("testid", "reset").
|
||||
Children(
|
||||
material_symbols.Refresh(),
|
||||
Text("Reset"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
emptyStore := &Store{EditRowIndex: -1}
|
||||
|
||||
examplesRouter.Get("/edit_row/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
contacts = starterEditContacts()
|
||||
datastar.RenderFragment(sse, contactsToNode(emptyStore))
|
||||
})
|
||||
|
||||
examplesRouter.Route("/edit_row/data", func(dataRouter chi.Router) {
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, contactsToNode(emptyStore))
|
||||
})
|
||||
|
||||
dataRouter.Get("/{index}", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
indexStr := chi.URLParam(r, "index")
|
||||
i, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error parsing index: %s", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
store := &Store{EditRowIndex: i, Name: contacts[i].Name, Email: contacts[i].Email}
|
||||
|
||||
datastar.RenderFragment(sse, contactsToNode(store))
|
||||
})
|
||||
})
|
||||
|
||||
examplesRouter.Route("/edit_row/edit", func(editRouter chi.Router) {
|
||||
editRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, &store); err != nil {
|
||||
http.Error(w, fmt.Sprintf("error unmarshalling contact : %s", err), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if store.EditRowIndex < 0 || store.EditRowIndex >= len(contacts) {
|
||||
http.Error(w, fmt.Sprintf("invalid index: %d", store.EditRowIndex), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
i := store.EditRowIndex
|
||||
c := contacts[i]
|
||||
store = &Store{EditRowIndex: i, Name: c.Name, Email: c.Email}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, contactsToNode(store))
|
||||
})
|
||||
|
||||
editRouter.Patch("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.BodyUnmarshal(r, &store); err != nil {
|
||||
http.Error(w, fmt.Sprintf("error unmarshalling store : %s", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if store.EditRowIndex < 0 || store.EditRowIndex >= len(contacts) {
|
||||
http.Error(w, fmt.Sprintf("invalid index: %d", store.EditRowIndex), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
i := store.EditRowIndex
|
||||
c := contacts[i]
|
||||
c.Name = store.Name
|
||||
c.Email = store.Email
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, contactsToNode(emptyStore))
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesFetchIndicator(examplesRouter chi.Router) error {
|
||||
|
||||
examplesRouter.Get("/fetch_indicator/greet", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, DIV().ID("greeting").Text(""))
|
||||
time.Sleep(2 * time.Second)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("greeting").
|
||||
TextF("Hello, the time is %s", time.Now().Format(time.RFC3339)),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesFileUpload(examplesRouter chi.Router) error {
|
||||
type Store struct {
|
||||
File []byte `json:"file"`
|
||||
FileMime string `json:"fileMime"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
examplesRouter.Get("/file_upload/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{
|
||||
File: []byte(""),
|
||||
}
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("file_upload").
|
||||
CLASS("flex flex-col gap-4").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
LABEL().
|
||||
CLASS("block mb-2 text-sm font-medium text-primary-100").
|
||||
FOR("file_input").
|
||||
Text("Pick anything reasonably sized"),
|
||||
INPUT().
|
||||
ID("file_input").
|
||||
TYPE("file").
|
||||
DATASTAR_MODEL("file").
|
||||
CLASS("block w-full text-sm border rounded-lg cursor-pointer text-primary-400 focus:outline-none bg-primary-700 border-primary-600 placeholder-primary-500"),
|
||||
),
|
||||
BUTTON().
|
||||
CLASS("bg-primary-300 hover:bg-primary-400 text-primary-800 font-bold py-2 px-4 rounded-l").
|
||||
Text("Submit").
|
||||
DATASTAR_ON("click", datastar.POST("/examples/file_upload/upload")),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
examplesRouter.Post("/file_upload/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 64*1024*1024)
|
||||
store := &Store{}
|
||||
err := datastar.BodyUnmarshal(r, store)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
if err != nil {
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("file_upload").
|
||||
CLASS("alert alert-error").
|
||||
Children(
|
||||
material_symbols.ErrorIcon(),
|
||||
TextF("Error: %s", err),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sha256Hash := sha256.Sum256(store.File)
|
||||
fileBytesLen := uint64(len(store.File))
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("file_upload").
|
||||
CLASS("card bg-base-300").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("card-body").
|
||||
Children(
|
||||
TABLE().
|
||||
CLASS("table table-zebra").
|
||||
Children(
|
||||
CAPTION(Text("File Upload Results")),
|
||||
TBODY().
|
||||
Children(
|
||||
TR(TH(Text("File Name")), TD(Text(store.FileName))),
|
||||
TR(TH(Text("File Size")), TD(Text(humanize.Bytes(fileBytesLen)))),
|
||||
TR(TH(Text("File Mime")), TD(Text(store.FileMime))),
|
||||
TR(
|
||||
TH(Text("SHA256 Hash")),
|
||||
TD().CLASS("text-ellipsis overflow-hidden").TextF("%x", sha256Hash),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesInfiniteScroll(examplesRouter chi.Router) error {
|
||||
|
||||
type Store struct {
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
renderAgentRow := func(i int) ElementRenderer {
|
||||
return TR().
|
||||
ID(fmt.Sprintf("agent_%d", i)).
|
||||
Children(
|
||||
TD(TextF("Agent Smith %x", i)),
|
||||
TD(TextF("void%d@null.org", i+1)),
|
||||
TD().CLASS("uppercase").TextF("%x", toolbelt.AliasHash(fmt.Sprint(i))),
|
||||
)
|
||||
}
|
||||
|
||||
renderAgentRows := func(input *Store) []ElementRenderer {
|
||||
arr := make([]ElementRenderer, input.Limit)
|
||||
for i := range arr {
|
||||
arr[i] = renderAgentRow(i + input.Offset)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
moreDiv := func(store *Store) ElementRenderer {
|
||||
return DIV().
|
||||
ID("more_btn").
|
||||
DATASTAR_INTERSECTS(fmt.Sprintf(
|
||||
"$offset=%d;$limit=%d;%s",
|
||||
store.Offset+store.Limit,
|
||||
store.Limit,
|
||||
datastar.GET("/examples/infinite_scroll/data"),
|
||||
)).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex justify-center text-4xl gap-2").
|
||||
CustomData("testid", "loading_message").
|
||||
Children(
|
||||
svg_spinners.BlocksWave(),
|
||||
Text("Loading..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
renderAgentsTable := func(store *Store) ElementRenderer {
|
||||
arr := make([]int, store.Limit)
|
||||
for i := range arr {
|
||||
arr[i] = i + store.Offset
|
||||
}
|
||||
|
||||
return DIV().
|
||||
ID("infinite_scroll").
|
||||
DATASTAR_STORE(store).
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
TABLE().
|
||||
CLASS("table w-full").
|
||||
Children(
|
||||
CAPTION(Text("Agents")),
|
||||
THEAD(
|
||||
TR(
|
||||
TH(Text("Name")),
|
||||
TH(Text("Email")),
|
||||
TH(Text("ID")),
|
||||
),
|
||||
),
|
||||
TBODY().
|
||||
ID("click_to_load_rows").
|
||||
Children(
|
||||
Group(renderAgentRows(store)...),
|
||||
),
|
||||
),
|
||||
moreDiv(store),
|
||||
)
|
||||
}
|
||||
|
||||
// infiniteScrollingRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// examplePage(w, r)
|
||||
// })
|
||||
|
||||
examplesRouter.Get("/infinite_scroll/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if store.Limit < 1 || store.Limit > 100 {
|
||||
store.Limit = 10
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
if store.Offset == 0 {
|
||||
datastar.RenderFragment(sse, renderAgentsTable(store))
|
||||
} else {
|
||||
time.Sleep(2 * time.Second)
|
||||
datastar.RenderFragment(
|
||||
sse, moreDiv(store),
|
||||
datastar.WithQuerySelectorID("more_btn"),
|
||||
)
|
||||
for _, node := range renderAgentRows(store) {
|
||||
datastar.RenderFragment(
|
||||
sse, node,
|
||||
datastar.WithQuerySelectorID("click_to_load_rows"),
|
||||
datastar.WithMergeAppendElement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExampleInlineValidation(examplesRouter chi.Router) error {
|
||||
|
||||
examplesRouter.Route("/inline_validation/data", func(dataRouter chi.Router) {
|
||||
|
||||
type User struct {
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
userValidation := func(u *User) (isEmailValid bool, isFirstNameValid bool, isLastNameValid bool, isValid bool) {
|
||||
isEmailValid = u.Email == "test@test.com"
|
||||
isFirstNameValid = len(u.FirstName) > 8
|
||||
isLastNameValid = len(u.LastName) > 8
|
||||
isValid = isFirstNameValid && isLastNameValid && isEmailValid
|
||||
return isEmailValid, isFirstNameValid, isLastNameValid, isValid
|
||||
}
|
||||
|
||||
fieldToNode := func(label, field string, isValid bool, isNotValidErrorLabelFmt string, labelArgs ...any) ElementRenderer {
|
||||
return DIV().
|
||||
CLASS("form-control").
|
||||
Children(
|
||||
LABEL().
|
||||
CLASS("label").
|
||||
Children(
|
||||
SPAN().
|
||||
CLASS("label-text").
|
||||
Text(label),
|
||||
),
|
||||
INPUT().
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5").
|
||||
IfCLASS(!isValid, "border-error").
|
||||
DATASTAR_MODEL(field).
|
||||
DATASTAR_ON("keydown", datastar.GET("/examples/inline_validation/data"), InputOnModDebounce(2*time.Second)).
|
||||
CustomData("testid", "input_"+field),
|
||||
If(!isValid, LABEL().CLASS("text-sm font-bold text-error-400").CustomData("testid", "validation_"+field).TextF(isNotValidErrorLabelFmt, labelArgs...)),
|
||||
)
|
||||
}
|
||||
|
||||
userToNode := func(u *User) ElementRenderer {
|
||||
isEmailValid, isFirstNameValid, isLastNameValid, isValid := userValidation(u)
|
||||
_, _ = isEmailValid, isFirstNameValid
|
||||
return DIV().
|
||||
ID("inline_validation").
|
||||
CLASS("flex flex-col gap-4").
|
||||
DATASTAR_STORE(u).
|
||||
Children(
|
||||
DIV().CLASS("font-bold text-2xl").Text("Sign Up"),
|
||||
DIV(
|
||||
fieldToNode(
|
||||
"Email Address",
|
||||
"email",
|
||||
isEmailValid,
|
||||
"Email '%s' is already taken or is invalid. Please enter another email.", u.Email,
|
||||
),
|
||||
fieldToNode(
|
||||
"First Name",
|
||||
"firstName",
|
||||
isFirstNameValid,
|
||||
"First name must be at least 8 characters.",
|
||||
),
|
||||
fieldToNode(
|
||||
"Last Name",
|
||||
"lastName",
|
||||
isLastNameValid,
|
||||
"Last name must be at least 8 characters.",
|
||||
),
|
||||
),
|
||||
BUTTON().
|
||||
CLASS("flex items-center gap-2 bg-success-300 hover:bg-success-400 text-success-800 font-bold py-2 px-4").
|
||||
IfCLASS(!isValid, "disabled").
|
||||
DATASTAR_ON("click", datastar.POST("/examples/inline_validation/data")).
|
||||
CustomData("testid", "submit_button").
|
||||
Children(
|
||||
material_symbols.PersonAdd(),
|
||||
Text("Submit"),
|
||||
),
|
||||
SignalStore,
|
||||
)
|
||||
}
|
||||
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
u := &User{}
|
||||
if err := datastar.QueryStringUnmarshal(r, u); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, userToNode(u))
|
||||
})
|
||||
|
||||
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
u := &User{}
|
||||
if err := datastar.BodyUnmarshal(r, u); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, _, isValid := userValidation(u)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
var node ElementRenderer
|
||||
if !isValid {
|
||||
node = userToNode(u)
|
||||
} else {
|
||||
node = DIV().
|
||||
ID("inline_validation").
|
||||
CLASS("font-bold text-4xl alert alert-success").
|
||||
Children(
|
||||
material_symbols.CheckCircle(),
|
||||
Text("Thank you for signing up!"),
|
||||
)
|
||||
}
|
||||
|
||||
datastar.RenderFragment(sse, node)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesIsLoadingId(examplesRouter chi.Router) error {
|
||||
|
||||
examplesRouter.Get("/is_loading_identifier/greet", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, DIV().ID("greeting").Text(""))
|
||||
time.Sleep(2 * time.Second)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("greeting").
|
||||
TextF("Hello, the time is %s", time.Now().Format(time.RFC3339)),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesLazyLoad(examplesRouter chi.Router) error {
|
||||
examplesRouter.Get("/lazy_load/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("lazy_load").
|
||||
DATASTAR_ON("load", datastar.GET("/examples/lazy_load/graph")).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex justify-center text-4xl gap-2").
|
||||
Children(
|
||||
svg_spinners.BlocksWave(),
|
||||
Text("Loading..."),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
examplesRouter.Get("/lazy_load/graph", func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(3 * time.Second)
|
||||
sp := staticPath("images/examples/tokyo.png")
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
IMG().
|
||||
ID("lazy_load").
|
||||
CLASS("transition-opacity").
|
||||
SRC(sp),
|
||||
datastar.WithSettleDuration(1*time.Second),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-faker/faker/v4"
|
||||
)
|
||||
|
||||
func setupExamplesLazyTabs(examplesRouter chi.Router) error {
|
||||
tabs := make([]ElementRenderer, 8)
|
||||
for i := range tabs {
|
||||
paragraphs := make([]ElementRenderer, 5)
|
||||
for j := range paragraphs {
|
||||
paragraphs[j] = P(Text(faker.Paragraph()))
|
||||
}
|
||||
tabs[i] = Group(paragraphs...)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
TabID int `json:"tabId"`
|
||||
}
|
||||
|
||||
tabsToNode := func(activeIdx int) ElementRenderer {
|
||||
log.Printf("tabsToNode: %d", activeIdx)
|
||||
return DIV().
|
||||
ID("lazy_tabs").
|
||||
DATASTAR_STORE(Store{TabID: activeIdx}).
|
||||
CLASS("flex flex-col").
|
||||
Children(
|
||||
UL().
|
||||
CLASS("list-none flex border-b-2 border-accent-700").
|
||||
Children(
|
||||
RangeI(tabs, func(i int, t ElementRenderer) ElementRenderer {
|
||||
return LI().
|
||||
CLASS("flex-1 px-2 cursor-pointer ").
|
||||
Children(
|
||||
A().
|
||||
CLASS("text-lg flex justify-center items-center inline-block border-l border-t border-r rounded-t py-px hover:bg-accent-500").
|
||||
IfCLASS(i == activeIdx, "bg-accent-500").
|
||||
IfCLASS(i != activeIdx, "bg-accent-700").
|
||||
TextF("Tab %d", i).
|
||||
CustomData("testid", fmt.Sprintf("tab_%d", i)).
|
||||
DATASTAR_ON(
|
||||
"click",
|
||||
fmt.Sprintf("$tabId=%d;$$get('/examples/lazy_tabs/data')", i),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
DIV().
|
||||
ID("tab_content").
|
||||
CLASS("p-4 shadow-lg bg-base-200").
|
||||
Children(tabs[activeIdx]),
|
||||
)
|
||||
}
|
||||
|
||||
examplesRouter.Get("/lazy_tabs/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, tabsToNode(store.TabID))
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
"github.com/zeebo/xxh3"
|
||||
)
|
||||
|
||||
func setupExamplesMergeOptions(examplesRouter chi.Router) error {
|
||||
setupContents := DIV().
|
||||
ID("contents").
|
||||
CLASS("flex flex-col gap-8").
|
||||
Children(
|
||||
DIV().ID("target").TextF("Target DIV"),
|
||||
DIV().CLASS("flex gap-2 flex-wrap").
|
||||
Children(
|
||||
Range(datastar.ValidFragmentMergeTypes, func(mergeMode datastar.FragmentMergeType) ElementRenderer {
|
||||
return BUTTON().
|
||||
CLASS("border-2 border-accent-500 px-4 py-2 rounded text-accent-200").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/merge_options/%s", mergeMode)).
|
||||
Text(string(mergeMode))
|
||||
}),
|
||||
),
|
||||
BUTTON().
|
||||
CLASS("bg-accent-500 px-4 py-2 rounded text-accent-200").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/merge_options/reset")).
|
||||
Text("Reset"),
|
||||
)
|
||||
|
||||
examplesRouter.Get("/merge_options/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
setupContents.Render(buf)
|
||||
log.Printf("contents: %s", buf.String())
|
||||
datastar.RenderFragment(sse, setupContents)
|
||||
})
|
||||
|
||||
brewerColorsBG := []string{
|
||||
"#a6cee3",
|
||||
"#1f78b4",
|
||||
"#b2df8a",
|
||||
"#33a02c",
|
||||
"#fb9a99",
|
||||
"#e31a1c",
|
||||
"#fdbf6f",
|
||||
"#ff7f00",
|
||||
"#cab2d6",
|
||||
"#6a3d9a",
|
||||
"#ffff99",
|
||||
"#b15928",
|
||||
}
|
||||
|
||||
brewrColorsFG := []string{
|
||||
"black",
|
||||
"white",
|
||||
"black",
|
||||
"black",
|
||||
"black",
|
||||
"white",
|
||||
"black",
|
||||
"black",
|
||||
"black",
|
||||
"white",
|
||||
"black",
|
||||
"white",
|
||||
}
|
||||
|
||||
examplesRouter.Get("/merge_options/{mergeMode}", func(w http.ResponseWriter, r *http.Request) {
|
||||
mergeModeRaw := chi.URLParam(r, "mergeMode")
|
||||
mergeMode := datastar.FragmentMergeType(mergeModeRaw)
|
||||
if !lo.Contains(datastar.ValidFragmentMergeTypes, mergeMode) {
|
||||
http.Error(w, "invalid merge mode", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
idx := lo.IndexOf(datastar.ValidFragmentMergeTypes, mergeMode)
|
||||
if mergeMode == datastar.FragmentMergeDeleteElement {
|
||||
datastar.Delete(sse, "#target")
|
||||
return
|
||||
} else {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
h := xxh3.HashString(now)
|
||||
updatedTarget := DIV().
|
||||
ID("target").
|
||||
STYLE("background-color", brewerColorsBG[idx]).
|
||||
STYLE("color", brewrColorsFG[idx]).
|
||||
CLASS("p-4 rounded").
|
||||
TextF("Update %x at %s", h, now)
|
||||
datastar.RenderFragment(sse, updatedTarget, datastar.WithMergeType(mergeMode))
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/martinusso/inflect"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setupExamplesModelBinding(examplesRouter chi.Router) error {
|
||||
type Store struct {
|
||||
BindText string `json:"bindText"`
|
||||
BindNumber int `json:"bindNumber"`
|
||||
BindBool bool `json:"bindBool"`
|
||||
BindSelection int `json:"bindSelection"`
|
||||
}
|
||||
|
||||
examplesRouter.Get("/model_binding/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
store := Store{
|
||||
BindText: "foo",
|
||||
BindNumber: 42,
|
||||
BindSelection: 1,
|
||||
BindBool: true,
|
||||
}
|
||||
|
||||
optionValues := lo.Range(7)
|
||||
selectOptions := lo.Map(optionValues, func(i, index int) ElementRenderer {
|
||||
return OPTION().VALUEF("%d", i).TextF("Option %d", i)
|
||||
})
|
||||
radioOptions := lo.Map(optionValues, func(i, index int) ElementRenderer {
|
||||
return LABEL().
|
||||
CLASS("font-brand font-bold text-xl flex items-center gap-2").
|
||||
Children(
|
||||
INPUT().
|
||||
TYPE("radio").
|
||||
DATASTAR_MODEL("bindSelection").
|
||||
VALUEF("%d", i),
|
||||
TextF("%s Option", inflect.Ordinalize(i)),
|
||||
)
|
||||
})
|
||||
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("container").
|
||||
CLASS("flex flex-col gap-4").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
INPUT().
|
||||
TYPE("text").
|
||||
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
|
||||
DATASTAR_MODEL("bindText"),
|
||||
INPUT().
|
||||
TYPE("number").
|
||||
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
|
||||
DATASTAR_MODEL("bindNumber"),
|
||||
TEXTAREA().
|
||||
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
|
||||
DATASTAR_MODEL("bindText"),
|
||||
LABEL().
|
||||
CLASS("flex items-center gap-1").
|
||||
Children(
|
||||
SPAN().Text("Checkbox"),
|
||||
INPUT().
|
||||
TYPE("checkbox").
|
||||
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
|
||||
DATASTAR_MODEL("bindBool"),
|
||||
),
|
||||
|
||||
SELECT().
|
||||
CLASS("border border-accent-500 bg-accent-700 rounded px-4 py-2 w-full py text-accent-200").
|
||||
DATASTAR_MODEL("bindSelection").
|
||||
Children(selectOptions...),
|
||||
DIV().
|
||||
CLASS("flex flex-col").
|
||||
Children(radioOptions...),
|
||||
|
||||
CODE().
|
||||
CLASS("border text-primary-200 border-primary-500 rounded p-8").
|
||||
Children(PRE().DATASTAR_TEXT("JSON.stringify(ctx.store(),null,2)")),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesMultilineFragments(examplesRouter chi.Router) error {
|
||||
examplesRouter.Get("/multiline_fragments/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragmentString(sse, `
|
||||
<div id="replaceMe">
|
||||
<pre>
|
||||
This is a multiline fragment.
|
||||
|
||||
Used when you are writing a lot of text by hand
|
||||
</pre>
|
||||
</div>
|
||||
`)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesProgressBar(examplesRouter chi.Router) error {
|
||||
|
||||
examplesRouter.Get("/progress_bar/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
progress := 0
|
||||
|
||||
for progress < 100 {
|
||||
progress = min(100, progress+rand.Intn(10)+1)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("progress_bar").
|
||||
Children(
|
||||
Tern(
|
||||
progress == 100,
|
||||
A().
|
||||
HREF("/examples/progress_bar").
|
||||
CLASS("btn btn-success").
|
||||
ID("completed_link").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex gap-2 font-bold text-2xl").
|
||||
Children(
|
||||
material_symbols.CheckCircle(),
|
||||
Text("Completed! Try again"),
|
||||
),
|
||||
),
|
||||
SVG_SVG().
|
||||
WIDTH("200").
|
||||
HEIGHT("200").
|
||||
Attr("viewbox", "-25 -25 250 250").
|
||||
STYLE("transform", "rotate(-90deg)").
|
||||
Children(
|
||||
SVG_CIRCLE().Attrs(
|
||||
"r", "90",
|
||||
"cx", "100",
|
||||
"cy", "100",
|
||||
"fill", "transparent",
|
||||
"stroke", "#e0e0e0",
|
||||
"stroke-width", "16px",
|
||||
"stroke-dasharray", "565.48px",
|
||||
"stroke-dashoffset", "565px",
|
||||
),
|
||||
SVG_CIRCLE().Attrs(
|
||||
"r", "90",
|
||||
"cx", "100",
|
||||
"cy", "100",
|
||||
"fill", "transparent",
|
||||
"stroke", "#6bdba7",
|
||||
"stroke-width", "16px",
|
||||
"stroke-linecap", "round",
|
||||
"stroke-dashoffset", fmt.Sprintf("%dpx", int(toolbelt.Fit(float32(progress), 0, 100, 565, 0))),
|
||||
"stroke-dasharray", "565.48px",
|
||||
),
|
||||
SVG_TEXT().Attrs(
|
||||
"x", "44px",
|
||||
"y", "115px",
|
||||
"fill", "#6bdba7",
|
||||
"font-size", "52px",
|
||||
"font-weight", "bold",
|
||||
"style", "transform:rotate(90deg) translate(0px, -196px)",
|
||||
).TextF("%d%%", progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
func setupExamplesQuickPrimerGo(examplesRouter chi.Router) error {
|
||||
examplesRouter.Route("/quick_primer_go/data", func(dataRouter chi.Router) {
|
||||
type Store struct {
|
||||
Input string `json:"input"`
|
||||
Show bool `json:"show"`
|
||||
}
|
||||
store := &Store{"initial backend data", false}
|
||||
|
||||
dataRouter.Get("/replace", func(w http.ResponseWriter, r *http.Request) {
|
||||
fragment := DIV().
|
||||
ID("replaceMe").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
H2().Text("Go Datastar Example"),
|
||||
MAIN().
|
||||
ID("main").
|
||||
CLASS("container flex flex-col gap-4").
|
||||
Children(
|
||||
INPUT().
|
||||
TYPE("text").
|
||||
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
|
||||
PLACEHOLDER("Type here!").
|
||||
DATASTAR_MODEL("input"),
|
||||
DIV().DATASTAR_TEXT("$input"),
|
||||
|
||||
BUTTON().
|
||||
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
|
||||
DATASTAR_ON("click", "$show = !$show").
|
||||
Text("Toggle"),
|
||||
DIV().
|
||||
Attr("data-show", "$show").
|
||||
Children(
|
||||
SPAN().Text("Hello from Datastar!"),
|
||||
),
|
||||
|
||||
DIV().ID("output").Text("#output"),
|
||||
BUTTON().
|
||||
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
|
||||
DATASTAR_ON("click", datastar.PUT("/examples/quick_primer_go/data")).
|
||||
Text("Send State"),
|
||||
|
||||
DIV().ID("output2").Text("#output2"),
|
||||
BUTTON().
|
||||
CLASS("p-2 border border-accent-400 rounded text-accent-200 bg-accent-700").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/quick_primer_go/data")).
|
||||
Text("Get State"),
|
||||
DIV().
|
||||
Children(
|
||||
SPAN().Text("Feed from server: "),
|
||||
SPAN().
|
||||
ID("feed").
|
||||
DATASTAR_ON("load", datastar.GET("/examples/quick_primer_go/data/feed")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, fragment)
|
||||
})
|
||||
|
||||
dataRouter.Put("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
reqStore := &Store{}
|
||||
if err := datastar.BodyUnmarshal(r, reqStore); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := sanitizer.Sanitize(reqStore); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
store = reqStore
|
||||
|
||||
fragment := DIV().
|
||||
ID("output").
|
||||
TextF("Your input: %s, is %d long.", store.Input, len(store.Input))
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(sse, fragment)
|
||||
})
|
||||
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
b, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fragment := fmt.Sprintf(`<div id="output2">Backend state: %s</div>`, string(b))
|
||||
datastar.RenderFragmentString(sse, fragment)
|
||||
|
||||
fragment = `<div>Check this out!</div>`
|
||||
datastar.RenderFragmentString(
|
||||
sse, fragment,
|
||||
datastar.WithQuerySelector("main"),
|
||||
datastar.WithMergePrependElement(),
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
dataRouter.Get("/feed", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
buf := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(buf, rand.Uint64())
|
||||
fragment := `<span id="feed">` + hex.EncodeToString(buf) + `</span>`
|
||||
|
||||
datastar.RenderFragmentString(sse, fragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesRedirects(examplesRouter chi.Router) error {
|
||||
|
||||
examplesRouter.Route("/redirects/data", func(dataRouter chi.Router) {
|
||||
type Store struct {
|
||||
RedirectTo string `json:"redirectTo"`
|
||||
}
|
||||
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{
|
||||
RedirectTo: "/essays/why_another_framework",
|
||||
}
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("demo").
|
||||
DATASTAR_STORE(store).
|
||||
CLASS("flex gap-4 w-full").
|
||||
Children(
|
||||
LABEL().
|
||||
CLASS("flex-1").
|
||||
Children(
|
||||
SPAN().Text("Redirect to: "),
|
||||
INPUT().
|
||||
DATASTAR_MODEL("redirectTo").
|
||||
CLASS("bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5"),
|
||||
),
|
||||
BUTTON().
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
|
||||
DATASTAR_ON("click", datastar.POST("/examples/redirects/data")).
|
||||
Text("Redirect"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.BodyUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
for i := 5; i > 0; i-- {
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().ID("demo").TextF("Redirecting in %d...", i),
|
||||
)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
datastar.Redirect(sse, store.RedirectTo)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
lorem "github.com/drhodes/golorem"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
func setupExamplesScrollIntoView(examplesRouter chi.Router) error {
|
||||
paragraphCount := 20
|
||||
paragraphs := make([]string, paragraphCount)
|
||||
for i := 0; i < paragraphCount; i++ {
|
||||
paragraphs[i] = lorem.Paragraph(40, 60)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Behavior string `json:"behavior"`
|
||||
Block string `json:"block"`
|
||||
Inline string `json:"inline"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Label string
|
||||
Values []string
|
||||
}
|
||||
|
||||
opts := []Options{
|
||||
{"behavior", []string{"smooth", "instant", "auto"}},
|
||||
{"block", []string{"vstart", "vcenter", "vend", "vnearest"}},
|
||||
{"inline", []string{"hstart", "hcenter", "hend", "hnearest"}},
|
||||
}
|
||||
// inlineOpts := []string{"hstart", "hcenter", "hend", "hnearest"}
|
||||
|
||||
examplesRouter.Route("/scroll_into_view/data", func(dataRouter chi.Router) {
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
store := &Store{
|
||||
Behavior: "smooth",
|
||||
Block: "vcenter",
|
||||
Inline: "hcenter",
|
||||
}
|
||||
|
||||
contents := DIV().
|
||||
ID("replaceMe").
|
||||
DATASTAR_STORE(store).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex flex-wrap gap-8 justify-center").
|
||||
Children(
|
||||
Range(opts, func(o Options) ElementRenderer {
|
||||
return DIV().
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
H3().Text(o.Label),
|
||||
SELECT().
|
||||
ID(o.Label).
|
||||
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
|
||||
DATASTAR_MODEL(o.Label).
|
||||
Children(
|
||||
Range(o.Values, func(v string) ElementRenderer {
|
||||
return OPTION().Text(v).VALUE(v)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
BUTTON().
|
||||
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
|
||||
Text("Scroll into view").
|
||||
DATASTAR_ON("click", datastar.PUT("/examples/scroll_into_view/data")),
|
||||
),
|
||||
RangeI(paragraphs, func(i int, p string) ElementRenderer {
|
||||
isMiddle := i == paragraphCount/2
|
||||
return Group(
|
||||
P().IDF("p%d", i).Text(p).IfCLASS(isMiddle, "bg-accent-800 text-accent-200 p-4 rounded-lg"),
|
||||
// If(isMiddle, A().HREF("#replaceMe").Text("Back to top")),
|
||||
)
|
||||
}),
|
||||
)
|
||||
datastar.RenderFragment(sse, contents)
|
||||
})
|
||||
|
||||
dataRouter.Put("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.BodyUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
attr := "scroll-into-view"
|
||||
if store.Behavior != "" {
|
||||
attr += "." + store.Behavior
|
||||
}
|
||||
if store.Block != "" {
|
||||
attr += "." + store.Block
|
||||
}
|
||||
if store.Inline != "" {
|
||||
attr += "." + store.Inline
|
||||
}
|
||||
|
||||
updated := P().IDF("p%d", paragraphCount/2).CustomData(attr, "")
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
updated.Render(buf)
|
||||
log.Println(buf.String())
|
||||
datastar.RenderFragment(sse, updated, datastar.WithMergeUpsertAttributes())
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setupExamplesShoelaceKitchensink(examplesRouter chi.Router) error {
|
||||
examplesRouter.Route("/shoelace_kitchensink/data", func(dataRouter chi.Router) {
|
||||
type Nested struct {
|
||||
Label string `json:"label"`
|
||||
Selection uint32 `json:"selection"`
|
||||
IsChecked bool `json:"isChecked"`
|
||||
}
|
||||
type Input struct {
|
||||
Nested *Nested `json:"nested"`
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Label string `json:"label"`
|
||||
Value uint32 `json:"value"`
|
||||
}
|
||||
|
||||
options := lo.Map(lo.Range(7), func(i, index int) Option {
|
||||
// offset := 100199071137923140 + int64(index)
|
||||
offset := toolbelt.NextID()
|
||||
return Option{
|
||||
Label: fmt.Sprintf("Option %d", i),
|
||||
Value: uint32(offset % math.MaxUint32),
|
||||
}
|
||||
})
|
||||
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
input := &Input{
|
||||
Nested: &Nested{
|
||||
Label: fmt.Sprintf("Hello World %d", rand.Intn(100)),
|
||||
Selection: options[rand.Intn(len(options))].Value,
|
||||
IsChecked: true,
|
||||
},
|
||||
}
|
||||
|
||||
datastar.RenderFragment(sse,
|
||||
DIV().
|
||||
ID("shoelace_kitchensink").
|
||||
CLASS("sl-theme-dark flex flex-col gap-4").
|
||||
DATASTAR_STORE(input).
|
||||
Children(
|
||||
SL_INPUT().
|
||||
LABEL("Label").
|
||||
DATASTAR_MODEL("nested.label"),
|
||||
SL_SELECT().
|
||||
LABEL("Select").
|
||||
DATASTAR_MODEL("nested.selection").
|
||||
DATASTAR_ON("sl-change", "console.log('change')").
|
||||
Children(
|
||||
Range(options, func(o Option) ElementRenderer {
|
||||
return SL_OPTION().
|
||||
VALUE(fmt.Sprint(o.Value)).
|
||||
TextF("%s (%d)", o.Label, o.Value)
|
||||
}),
|
||||
),
|
||||
SL_RADIOGROUP().
|
||||
LABEL("Radio Group").
|
||||
DATASTAR_MODEL("nested.selection").
|
||||
Children(
|
||||
Range(options, func(o Option) ElementRenderer {
|
||||
return SL_RADIOBUTTON().
|
||||
VALUE(fmt.Sprint(o.Value)).
|
||||
Text(o.Label)
|
||||
}),
|
||||
),
|
||||
SL_CHECKBOX().
|
||||
DATASTAR_MODEL("nested.isChecked").
|
||||
Text("Checkbox"),
|
||||
SL_BUTTON().
|
||||
VARIANT(SLButtonVariant_primary).
|
||||
Text("Submit").
|
||||
DATASTAR_ON("click", datastar.POST(r.URL.Path)),
|
||||
SignalStore,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := datastar.NewSSE(w, r)
|
||||
var res any
|
||||
if err := datastar.BodyUnmarshal(r, &res); err != nil {
|
||||
datastar.Error(sse, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("res: %#v", res)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesTitleSelect(examplesRouter chi.Router) error {
|
||||
examplesRouter.Get("/title_select/updates", func(w http.ResponseWriter, r *http.Request) {
|
||||
// You can comment out the below block and still persist the session
|
||||
sse := datastar.NewSSE(w, r)
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-t.C:
|
||||
datastar.RenderFragment(sse,
|
||||
TITLE().TextF("%s from server", time.Now().Format(time.TimeOnly)),
|
||||
datastar.WithQuerySelector("title"),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setupExamplesValueSelect(examplesRouter chi.Router) error {
|
||||
|
||||
// lazyLoadRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// examplePage(w, r)
|
||||
// })
|
||||
|
||||
type Model struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Make struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Models []*Model `json:"models"`
|
||||
}
|
||||
|
||||
cars := []*Make{
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Audi",
|
||||
Models: []*Model{
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "A1",
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "A3",
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "A6",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Toyota",
|
||||
Models: []*Model{
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Land Cruiser",
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Corolla",
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Camry",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Ford",
|
||||
Models: []*Model{
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "F-150",
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Mustang",
|
||||
},
|
||||
{
|
||||
ID: toolbelt.NextEncodedID(),
|
||||
Label: "Focus",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Make string `json:"make"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
storeValidation := func(store *Store) (make *Make, model *Model, isValid bool) {
|
||||
if store.Make != "" {
|
||||
for _, possibleMake := range cars {
|
||||
if possibleMake.ID == store.Make {
|
||||
make = possibleMake
|
||||
|
||||
if store.Model != "" {
|
||||
for _, possibleModel := range make.Models {
|
||||
if possibleModel.ID == store.Model {
|
||||
model = possibleModel
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return make, model, isValid
|
||||
}
|
||||
|
||||
examplesRouter.Route("/value_select/data", func(dataRouter chi.Router) {
|
||||
|
||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
make, model, isValid := storeValidation(store)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
datastar.RenderFragment(
|
||||
sse,
|
||||
DIV().
|
||||
ID("value_select").
|
||||
DATASTAR_STORE(store).
|
||||
CLASS("flex flex-col gap-2").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("text-2xl font-bold").
|
||||
Text("Pick a Make / Model"),
|
||||
SELECT().
|
||||
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
|
||||
DATASTAR_MODEL("make").
|
||||
DATASTAR_ON("change", datastar.GET("/examples/value_select/data")).
|
||||
CustomData("testid", "make_select").
|
||||
Children(
|
||||
OPTION().
|
||||
DISABLED().
|
||||
SELECTED().
|
||||
Text("Select a Make").
|
||||
VALUE(""),
|
||||
Group(Range(cars, func(item *Make) ElementRenderer {
|
||||
return OPTION().
|
||||
VALUE(item.ID).
|
||||
Text(item.Label).
|
||||
CustomData("testid", "make_option_"+item.Label)
|
||||
})),
|
||||
),
|
||||
DynIf(
|
||||
make != nil,
|
||||
func() ElementRenderer {
|
||||
return SELECT().
|
||||
CLASS("bg-accent-800 border border-accent-600 text-accent-200 text-sm rounded-lg focus:ring-accent-400 focus:border-accent-400 block w-full p-2.5").
|
||||
DATASTAR_MODEL("model").
|
||||
DATASTAR_ON("change", datastar.GET("/examples/value_select/data")).
|
||||
CustomData("testid", "model_select").
|
||||
Children(
|
||||
OPTION().
|
||||
DISABLED().
|
||||
SELECTED().
|
||||
Text("Select a Model").
|
||||
VALUE(""),
|
||||
Group(Range(make.Models, func(item *Model) ElementRenderer {
|
||||
return OPTION().
|
||||
VALUE(item.ID).
|
||||
Text(item.Label).
|
||||
CustomData("testid", "model_option_"+item.Label)
|
||||
})),
|
||||
)
|
||||
},
|
||||
),
|
||||
DynIf(
|
||||
isValid,
|
||||
func() ElementRenderer {
|
||||
return BUTTON().
|
||||
CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
|
||||
DATASTAR_ON("click", datastar.POST("/examples/value_select/data")).
|
||||
CustomData("testid", "select_button").
|
||||
Children(
|
||||
material_symbols.CarRepair(),
|
||||
TextF("Submit selected '%s / %s' choice", make.Label, model.Label),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
store := &Store{}
|
||||
if err := datastar.BodyUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, isValid := storeValidation(store)
|
||||
|
||||
if !isValid {
|
||||
http.Error(w, "invalid input", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
make, ok := lo.Find(cars, func(item *Make) bool {
|
||||
return item.ID == store.Make
|
||||
})
|
||||
if !ok {
|
||||
http.Error(w, "invalid input", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
model, ok := lo.Find(make.Models, func(item *Model) bool {
|
||||
return item.ID == store.Model
|
||||
})
|
||||
|
||||
if !ok {
|
||||
http.Error(w, "invalid input", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
datastar.RenderFragment(sse,
|
||||
DIV().
|
||||
ID("value_select").
|
||||
Children(
|
||||
Text("You selected"),
|
||||
BR(),
|
||||
TextF("Make '%s' db id:%s", make.Label, make.ID),
|
||||
BR(),
|
||||
TextF("Model '%s' db id:%s", model.Label, model.ID),
|
||||
BUTTON().CLASS("flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-success-700 hover:bg-success-600").
|
||||
DATASTAR_ON("click", datastar.GET("/examples/value_select/data")).
|
||||
Children(
|
||||
material_symbols.ResetWrench(),
|
||||
TextF("Resest form"),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupExamplesViewTransitionAPI(examplesRouter chi.Router) error {
|
||||
|
||||
type Store struct {
|
||||
UseSlide bool `json:"useSlide"`
|
||||
}
|
||||
|
||||
examplesRouter.Get("/view_transition_api/watch", func(w http.ResponseWriter, r *http.Request) {
|
||||
// You can comment out the below block and still persist the session
|
||||
|
||||
store := &Store{}
|
||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
t := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-t.C:
|
||||
datastar.RenderFragment(sse, DIV().
|
||||
ID("stuff").
|
||||
CLASS("font-brand font-bold text-2xl flex flex-col gap-4").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("flex gap-2").
|
||||
Children(
|
||||
DIV().Text("The time is:"),
|
||||
DIV().
|
||||
CLASS("text-primary-300").
|
||||
IfDATASTAR_VIEW_TRANSITION(
|
||||
store.UseSlide,
|
||||
"'slide-it'",
|
||||
).
|
||||
Text(time.Now().Format(time.TimeOnly)),
|
||||
),
|
||||
BUTTON().
|
||||
DATASTAR_ON("click", "location.reload()").
|
||||
CLASS("bg-primary-600 hover:bg-primary-700 flex flex-col justify-center items-center no-underline font-brand font-bold w-full p-4 cursor-pointer text-accent-50 rounded-md text-center flex gap-2 items-center justify-center").
|
||||
Text("Reload"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
|
||||
"github.com/delaneyj/datastar"
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/material_symbols"
|
||||
"github.com/delaneyj/gostar/elements/iconify/simple_icons"
|
||||
"github.com/delaneyj/gostar/elements/iconify/svg_spinners"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
func setupHome(router chi.Router) error {
|
||||
|
||||
chartWidth := 480
|
||||
graph := chart.BarChart{
|
||||
Title: "File Size (Hello World) first load",
|
||||
Width: chartWidth,
|
||||
Height: chartWidth,
|
||||
Background: chart.Style{
|
||||
FillColor: drawing.Color{R: 1, G: 1, B: 1, A: 0},
|
||||
FontColor: drawing.ColorWhite,
|
||||
},
|
||||
Canvas: chart.Style{
|
||||
FillColor: drawing.Color{R: 1, G: 1, B: 1, A: 0},
|
||||
FontColor: drawing.ColorWhite,
|
||||
FontSize: 6,
|
||||
},
|
||||
TitleStyle: chart.Style{
|
||||
FontColor: drawing.ColorWhite,
|
||||
},
|
||||
XAxis: chart.Style{
|
||||
FontColor: drawing.ColorWhite,
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Style: chart.Style{
|
||||
FontColor: drawing.ColorWhite,
|
||||
},
|
||||
ValueFormatter: func(v any) string {
|
||||
return humanize.Bytes(uint64(v.(float64)))
|
||||
},
|
||||
},
|
||||
Bars: []chart.Value{
|
||||
{Label: "Next.js", Value: 86221},
|
||||
{Label: "SvelteKit", Value: 81920},
|
||||
{Label: "HTMX+\nhyperscript", Value: 40653},
|
||||
{Label: "HTMX+\nAlpine", Value: 37980},
|
||||
{Label: "Datastar", Value: 10445},
|
||||
{Label: "Datastar Core", Value: 4526},
|
||||
},
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
err := graph.Render(chart.SVG, buffer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
svgChart := buffer.String()
|
||||
|
||||
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
cdnText := `<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar" ></script>`
|
||||
page(
|
||||
DIV().CLASS("min-h-screen md:flex md:items-center flex-col md:p-8 bg-gradient-to-br from-accent-700 to-accent-900").Children(
|
||||
DIV().
|
||||
CLASS("p-4 md:max-w-md md:max-w-2xl md:flex flex-col gap-8 md:items-center").
|
||||
Children(
|
||||
datastarLogo().CLASS(
|
||||
"w-24 md:w-64 md:h-64 fill-current text-accent-200",
|
||||
"animate__animated animate__pulse animate__infinite animate__faster",
|
||||
),
|
||||
DIV().
|
||||
CLASS("font-brand font-bold text-3xl md:text-6xl text-primary-200").
|
||||
Text("DATASTAR"),
|
||||
DIV().CLASS("font-brand text-lg").Text("Real-time hypermedia framework"),
|
||||
P().
|
||||
CLASS("text-primary-100 md:text-center").
|
||||
TextF(
|
||||
`Using a single <span class="text-lg font-bold text-primary-300">%s</span> CDN link and have access to everything needed to rival a full-stack SPA framework; all in the language of your choice.`,
|
||||
iifeBuildSize,
|
||||
),
|
||||
DIV().CLASS("flex flex-col justify-center gap-4 w-full").Children(
|
||||
buttonLink(true).
|
||||
CLASS("w-full").
|
||||
HREF("https://discord.gg/CHvPMrAp6F").
|
||||
Children(
|
||||
simple_icons.Discord(),
|
||||
SPAN().Text("Join the conversation"),
|
||||
).
|
||||
TARGET("_blank").
|
||||
REL("noopener", "noreferrer"),
|
||||
buttonLink(true).
|
||||
CLASS("w-full").
|
||||
HREF("https://github.com/delaneyj/datastar/tree/main/library/src/lib").
|
||||
Children(
|
||||
simple_icons.Github(),
|
||||
SPAN().Text("Check out the source!"),
|
||||
).
|
||||
TARGET("_blank").
|
||||
REL("noopener", "noreferrer"),
|
||||
),
|
||||
DIV().
|
||||
CLASS("p-4 rounded bg-primary-900 text-xs text-primary-200 flex flex-col gap-2 cursor-pointer items-center").
|
||||
DATASTAR_ON("click", html.EscapeString(fmt.Sprintf("$$clipboard('%s')", cdnText))).
|
||||
Children(
|
||||
SPAN().CLASS("font-bold italic text-primary-300").Text("Just add to your HTML:"),
|
||||
CODE().CLASS("flex justify-center items-center gap-4").
|
||||
Children(
|
||||
material_symbols.ContentCopy().CLASS("text-4xl"),
|
||||
Escaped(cdnText),
|
||||
),
|
||||
),
|
||||
DIV().CLASS("md:flex md:justify-center md:items-center md:p-4").Text(svgChart),
|
||||
DIV().
|
||||
CLASS(
|
||||
"flex flex-col gap-6 border-dashed border-2 border-primary-300 p-4 rounded-lg",
|
||||
"bg-gradient-to-br from-primary-800 to-primary-900",
|
||||
).
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("text-2xl font-bold").
|
||||
Children(Text("Example of a dynamically loaded area of page with shared global state")),
|
||||
DIV().
|
||||
ID("global-count-example").
|
||||
CLASS("flex justify-center p-4 items-center gap-4").
|
||||
DATASTAR_ON("load", datastar.GET("/api/globalCount")).
|
||||
Children(
|
||||
SPAN().
|
||||
CLASS("text-2xl").
|
||||
Text("Loading example on delay..."),
|
||||
svg_spinners.Eclipse().ID("spinner"),
|
||||
),
|
||||
),
|
||||
buttonLink().
|
||||
CLASS("w-full").
|
||||
HREF("/docs/"+docNames[0]).
|
||||
Text("Let's Get Started"),
|
||||
).CLASS("flex flex-col justify-center items-center"),
|
||||
),
|
||||
).Render(w)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setupReferenceRoutes(router chi.Router) error {
|
||||
mdElementRenderers, mdAnchors, err := markdownRenders("reference")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Reference struct {
|
||||
ID string
|
||||
Label string
|
||||
URL string
|
||||
Description string
|
||||
Prev, Next *Reference
|
||||
}
|
||||
type ReferenceGroup struct {
|
||||
Label string
|
||||
References []*Reference
|
||||
}
|
||||
|
||||
var (
|
||||
prevRef *Reference
|
||||
referenceByURL = map[string]*Reference{}
|
||||
)
|
||||
references := lo.Map([]ReferenceGroup{
|
||||
{
|
||||
Label: "Included Plugins",
|
||||
References: []*Reference{
|
||||
{ID: "plugins_core", Label: "Core"},
|
||||
{ID: "plugins_attributes", Label: "Attributes"},
|
||||
{ID: "plugins_backend", Label: "Backend"},
|
||||
{ID: "plugins_helpers", Label: "Helpers"},
|
||||
{ID: "plugins_visibility", Label: "Visibility"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "How it Works",
|
||||
References: []*Reference{
|
||||
{ID: "expressions", Label: "Expressions"},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// Label: "Writing Plugins",
|
||||
// References: []*Reference{},
|
||||
// },
|
||||
}, func(g ReferenceGroup, i int) ReferenceGroup {
|
||||
for _, example := range g.References {
|
||||
example.URL = "/reference/" + example.ID
|
||||
if prevRef != nil {
|
||||
example.Prev = prevRef
|
||||
prevRef.Next = example
|
||||
}
|
||||
prevRef = example
|
||||
referenceByURL[example.URL] = example
|
||||
}
|
||||
return g
|
||||
})
|
||||
|
||||
router.Route("/reference", func(referenceRouter chi.Router) {
|
||||
referenceRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, references[0].References[0].URL, http.StatusFound)
|
||||
})
|
||||
|
||||
sidebarContents := func(r *http.Request) ElementRenderer {
|
||||
return Range(references, func(g ReferenceGroup) ElementRenderer {
|
||||
return DIV(
|
||||
DIV(
|
||||
DIV().CLASS("text-2xl font-bold text-primary").Text(g.Label),
|
||||
HR().CLASS("divider border-primary"),
|
||||
),
|
||||
UL().
|
||||
CLASS("list-disc pl-4").
|
||||
Children(Range(g.References, func(e *Reference) ElementRenderer {
|
||||
return LI(
|
||||
link(e.URL, e.Label, e.URL == r.URL.Path),
|
||||
)
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
referenceRouter.Get("/{refName}", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ref, ok := referenceByURL[r.URL.Path]
|
||||
if !ok {
|
||||
ref = references[0].References[0]
|
||||
}
|
||||
|
||||
contents, ok := mdElementRenderers[ref.ID]
|
||||
if !ok {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
contentGroup := []ElementRenderer{}
|
||||
if ref.Prev != nil {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full no-underline").
|
||||
HREF(ref.Prev.URL).
|
||||
Text("Back to "+ref.Prev.Label),
|
||||
)
|
||||
}
|
||||
contentGroup = append(contentGroup, contents)
|
||||
if ref.Next != nil {
|
||||
contentGroup = append(contentGroup,
|
||||
buttonLink().
|
||||
CLASS("w-full no-underline").
|
||||
HREF(ref.Next.URL).
|
||||
Text("Next "+ref.Next.Label),
|
||||
)
|
||||
}
|
||||
|
||||
anchors := mdAnchors[ref.ID]
|
||||
|
||||
prosePage(r, sidebarContents(r), Group(contentGroup...), anchors).Render(w)
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
. "github.com/delaneyj/gostar/elements"
|
||||
"github.com/delaneyj/gostar/elements/iconify/mdi"
|
||||
"github.com/delaneyj/gostar/elements/iconify/simple_icons"
|
||||
"github.com/delaneyj/toolbelt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var isDEV = os.Getenv("ENV") == "dev"
|
||||
|
||||
func page(children ...ElementRenderer) ElementRenderer {
|
||||
|
||||
return Group(
|
||||
NewElement("!DOCTYPE").Attr("html", ""),
|
||||
HTML().
|
||||
LANG("en").
|
||||
Children(
|
||||
TITLE().Text("DATASTAR"),
|
||||
HEAD(
|
||||
META().CHARSET("UTF-8"),
|
||||
META().NAME("viewport").CONTENT("width=device-width, initial-scale=1"),
|
||||
META().NAME("description").CONTENT("Datastar is declarative real-time hypermedia framework"),
|
||||
LINK().REL("icon").HREF(staticPath("images/datastar_icon.svg")),
|
||||
LINK().REL("stylesheet").HREF(staticPath("css/site.css")),
|
||||
cdnLink("@unocss/reset/tailwind.min.css"),
|
||||
cdnLink("animate.css@4.1.1/animate.min.css"),
|
||||
STYLE().Text(`
|
||||
.un-cloak {
|
||||
display: none;
|
||||
}
|
||||
`),
|
||||
),
|
||||
BODY().
|
||||
CLASS("font-sans min-h-screen un-cloak").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("bg-accent-900 text-primary-50 w-full h-full").
|
||||
Children(children...),
|
||||
),
|
||||
If(isDEV,
|
||||
SCRIPT(Text(`
|
||||
function initHotReload() {
|
||||
console.log("Hot reload initializing")
|
||||
if (typeof(EventSource) !== "undefined") {
|
||||
const es = new EventSource("/hotreload");
|
||||
es.onmessage = function(event) {
|
||||
location.reload();
|
||||
}
|
||||
es.onerror = function(err) {
|
||||
console.log("lost connection to server, reloading");
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 250);
|
||||
};
|
||||
}
|
||||
}
|
||||
initHotReload();
|
||||
// `)),
|
||||
),
|
||||
SCRIPT().
|
||||
TYPE("module").
|
||||
SRC(staticPath("library/datastar.js")).
|
||||
DEFER(),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func cdnLink(css string) ElementRenderer {
|
||||
return LINK().REL("stylesheet").HREF("https://cdn.jsdelivr.net/npm/" + css)
|
||||
}
|
||||
|
||||
var iifeBuildSize string
|
||||
|
||||
func upsertIIfeBuildSize() string {
|
||||
if iifeBuildSize != "" {
|
||||
return iifeBuildSize
|
||||
}
|
||||
build, err := staticFS.ReadFile("static/library/datastar.iife.js")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
w, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if _, err := w.Write(build); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w.Close()
|
||||
log.Printf("iife build size: %s", humanize.IBytes(uint64(buf.Len())))
|
||||
iifeBuildSize = humanize.IBytes(uint64(buf.Len()))
|
||||
return iifeBuildSize
|
||||
}
|
||||
|
||||
func markdownRenders(staticMdPath string) (mdElementRenderers map[string]ElementRenderer, mdAnchors map[string][]string, err error) {
|
||||
mdDir := "static/md/" + staticMdPath
|
||||
docs, err := staticFS.ReadDir(mdDir)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error reading docs dir: %w", err)
|
||||
}
|
||||
|
||||
// regExpImg := regexp.MustCompile(`(?P<whole>!\[[^\]]+]\((?P<path>[^)]+)\))`)
|
||||
// prefix := []byte("/static/")
|
||||
|
||||
mdElementRenderers = map[string]ElementRenderer{}
|
||||
mdAnchors = map[string][]string{}
|
||||
for _, de := range docs {
|
||||
fullPath := mdDir + "/" + de.Name()
|
||||
|
||||
b, err := staticFS.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error reading doc %s: %w", de.Name(), err)
|
||||
}
|
||||
|
||||
anchors := []string{}
|
||||
lines := strings.Split(string(b), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "#") {
|
||||
parts := strings.Split(line, " ")
|
||||
anchor := strings.Join(parts[1:], " ")
|
||||
anchors = append(anchors, anchor)
|
||||
}
|
||||
}
|
||||
|
||||
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Footnotes)
|
||||
doc := mdParser.Parse(b)
|
||||
renderedHTML := string(markdown.Render(doc, mdRenderer()))
|
||||
|
||||
name := de.Name()[0 : len(de.Name())-3]
|
||||
mdElementRenderers[name] = Text(renderedHTML)
|
||||
mdAnchors[name] = anchors
|
||||
}
|
||||
|
||||
return mdElementRenderers, mdAnchors, nil
|
||||
}
|
||||
|
||||
func header(r *http.Request) ElementRenderer {
|
||||
linkChildren := []ElementRenderer{
|
||||
linkChild("https://discord.gg/CHvPMrAp6F", simple_icons.Discord()).TARGET("_blank").REL("noopener", "noreferrer"),
|
||||
linkChild("https://github.com/delaneyj/datastar/tree/main/library", simple_icons.Github()).TARGET("_blank").REL("noopener", "noreferrer"),
|
||||
linkChild("https://www.npmjs.com/package/@sudodevnull/datastar", simple_icons.Npm()).TARGET("_blank").REL("noopener", "noreferrer"),
|
||||
}
|
||||
|
||||
topLevelLinks := lo.Map([]string{
|
||||
"docs",
|
||||
"reference",
|
||||
"examples",
|
||||
"essays",
|
||||
}, func(name string, i int) ElementRenderer {
|
||||
return link("/"+name, name, strings.HasPrefix(r.URL.Path, "/"+name))
|
||||
})
|
||||
|
||||
return DIV().
|
||||
CLASS("shadow-lg").
|
||||
Children(
|
||||
HEADER().CLASS("bg-accent-700 text-accent-200 px-4 py-2 flex flex-wrap gap-2 justify-between items-center").Children(
|
||||
DIV().CLASS("flex flex-wrap gap-2 items-end").Children(
|
||||
A().CLASS("flex gap-1").
|
||||
HREF("/").
|
||||
Children(
|
||||
DIV().CLASS("font-brand font-bold text-2xl uppercase hidden md:block").Text("Datastar"),
|
||||
datastarLogo().CLASS("h-8"),
|
||||
),
|
||||
DIV().
|
||||
CLASS("font-mono text-accent-300").
|
||||
TextF("v%s", packageJSON.Version),
|
||||
),
|
||||
DIV().
|
||||
CLASS("flex flex-wrap gap-3 uppercase font-brand text-xs md:text-sm lg:text-lg items-center").
|
||||
Children(topLevelLinks...),
|
||||
|
||||
DIV().CLASS("hidden md:flex").Children(
|
||||
linkChildren...,
|
||||
),
|
||||
),
|
||||
HEADER().CLASS("md:hidden bg-accent-800 text-accent-200 px-4 py-2 flex flex-wrap gap-2 justify-between items-center").Children(
|
||||
BUTTON().
|
||||
// CustomData("show", "!$sidebarOpen;debugger;!x").
|
||||
DATASTAR_ON("click", "$sidebarOpen = true").
|
||||
CLASS("bg-accent-600 hover:bg-accent-700 text-primary-50 p-2 rounded-md").
|
||||
Children(mdi.Menu()),
|
||||
DIV().CLASS("flex gap-1 text-2xl").Children(linkChildren...),
|
||||
),
|
||||
)
|
||||
}
|
||||
func prosePage(r *http.Request, sidebarContents ElementRenderer, contents ElementRenderer, asideAnchors []string) ElementRenderer {
|
||||
log.Print(r.URL.Path)
|
||||
|
||||
return page(
|
||||
highlightCSS,
|
||||
DIV().
|
||||
DATASTAR_STORE(map[string]any{
|
||||
"sidebarOpen": false,
|
||||
}).
|
||||
CLASS("grid grid-rows-[auto_1fr] h-screen").
|
||||
Children(
|
||||
header(r),
|
||||
DIV().
|
||||
CLASS("flex justify-start overflow-hidden relative").
|
||||
Children(
|
||||
DynIf(sidebarContents != nil, func() ElementRenderer {
|
||||
return Group(
|
||||
DIV().
|
||||
CLASS("fixed inset-0 z-40 md:hidden").
|
||||
CustomData("show", "$sidebarOpen").
|
||||
Children(
|
||||
ASIDE().
|
||||
CLASS("px-4 py-8 w-64 bg-accent-800 text-accent-200 relative z-30 h-full flex flex-col gap-4").
|
||||
Children(
|
||||
|
||||
IMG().SRC(staticPath("images/datastar.svg")).ALT("logo").CLASS("h-8"),
|
||||
DIV(sidebarContents).CLASS("overflow-y-auto h-full flex flex-col"),
|
||||
),
|
||||
DIV().
|
||||
DATASTAR_ON("click", "$sidebarOpen = false").
|
||||
CLASS("fixed inset-0 bg-primary-900 bg-opacity-70 z-10"),
|
||||
),
|
||||
ASIDE(sidebarContents).
|
||||
CLASS("hidden md:flex flex-col px-4 py-8 min-w-64 bg-accent-800 text-accent-200 hidden md:visible gap-4 overflow-y-auto"),
|
||||
)
|
||||
}),
|
||||
DIV().
|
||||
CLASS("md:flex md:justify-center px-2 py-4 md:px-4 w-full overflow-y-scroll scroll-mb-16").
|
||||
Children(
|
||||
// SignalStore,
|
||||
DIV(contents).CLASS("prose prose-xs md:prose-2xl"),
|
||||
),
|
||||
DynIf(len(asideAnchors) > 0, func() ElementRenderer {
|
||||
return ASIDE().
|
||||
CLASS("min-w-52 py-4 h-screen text-accent-200 hidden lg:block transition-all").
|
||||
Children(
|
||||
DIV().
|
||||
CLASS("h-full border-l-2 border-accent-600 px-4 py-4 flex flex-col gap-4").
|
||||
Children(
|
||||
DIV().CLASS("border-b border-accent-600 w-full ").Text("On this page"),
|
||||
Range(asideAnchors, func(anchor string) ElementRenderer {
|
||||
kebab := toolbelt.Kebab(anchor)
|
||||
return link("#"+kebab, anchor, false).CLASS("font-light text-sm")
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
/* layer: preflights */
|
||||
*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
|
||||
/* layer: typography */
|
||||
.prose :where(h1,h2,h3,h4,h5,h6):not(:where(.not-prose,.not-prose *)){color:var(--un-prose-headings);font-weight:600;line-height:1.25;}.prose :where(a):not(:where(.not-prose,.not-prose *)){color:var(--un-prose-links);text-decoration:underline;font-weight:500;}.prose :where(a code):not(:where(.not-prose,.not-prose *)){color:var(--un-prose-links);}.prose :where(p,ul,ol,pre):not(:where(.not-prose,.not-prose *)){margin:1em 0;line-height:1.75;}.prose :where(blockquote):not(:where(.not-prose,.not-prose *)){margin:1em 0;padding-left:1em;font-style:italic;border-left:.25em solid var(--un-prose-borders);}.prose :where(h1):not(:where(.not-prose,.not-prose *)){margin:1rem 0;font-size:2.25em;}.prose :where(h2):not(:where(.not-prose,.not-prose *)){margin:1.75em 0 .5em;font-size:1.75em;}.prose :where(h3):not(:where(.not-prose,.not-prose *)){margin:1.5em 0 .5em;font-size:1.375em;}.prose :where(h4):not(:where(.not-prose,.not-prose *)){margin:1em 0;font-size:1.125em;}.prose :where(img,video):not(:where(.not-prose,.not-prose *)){max-width:100%;}.prose :where(figure,picture):not(:where(.not-prose,.not-prose *)){margin:1em 0;}.prose :where(figcaption):not(:where(.not-prose,.not-prose *)){color:var(--un-prose-captions);font-size:.875em;}.prose :where(code):not(:where(.not-prose,.not-prose *)){color:var(--un-prose-code);font-size:.875em;font-weight:600;font-family:"Fira Code",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;}.prose :where(:not(pre) > code):not(:where(.not-prose,.not-prose *))::before,.prose :where(:not(pre) > code):not(:where(.not-prose,.not-prose *))::after{content:"`";}.prose :where(pre):not(:where(.not-prose,.not-prose *)){padding:1.25rem 1.5rem;overflow-x:auto;border-radius:.375rem;}.prose :where(pre,code):not(:where(.not-prose,.not-prose *)){white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;hyphens:none;background:transparent;}.prose :where(pre code):not(:where(.not-prose,.not-prose *)){font-weight:inherit;}.prose :where(ol,ul):not(:where(.not-prose,.not-prose *)){padding-left:1.25em;}.prose :where(ol):not(:where(.not-prose,.not-prose *)){list-style-type:decimal;}.prose :where(ol[type="A"]):not(:where(.not-prose,.not-prose *)){list-style-type:upper-alpha;}.prose :where(ol[type="a"]):not(:where(.not-prose,.not-prose *)){list-style-type:lower-alpha;}.prose :where(ol[type="A" s]):not(:where(.not-prose,.not-prose *)){list-style-type:upper-alpha;}.prose :where(ol[type="a" s]):not(:where(.not-prose,.not-prose *)){list-style-type:lower-alpha;}.prose :where(ol[type="I"]):not(:where(.not-prose,.not-prose *)){list-style-type:upper-roman;}.prose :where(ol[type="i"]):not(:where(.not-prose,.not-prose *)){list-style-type:lower-roman;}.prose :where(ol[type="I" s]):not(:where(.not-prose,.not-prose *)){list-style-type:upper-roman;}.prose :where(ol[type="i" s]):not(:where(.not-prose,.not-prose *)){list-style-type:lower-roman;}.prose :where(ol[type="1"]):not(:where(.not-prose,.not-prose *)){list-style-type:decimal;}.prose :where(ul):not(:where(.not-prose,.not-prose *)){list-style-type:disc;}.prose :where(ol > li):not(:where(.not-prose,.not-prose *))::marker,.prose :where(ul > li):not(:where(.not-prose,.not-prose *))::marker,.prose :where(summary):not(:where(.not-prose,.not-prose *))::marker{color:var(--un-prose-lists);}.prose :where(hr):not(:where(.not-prose,.not-prose *)){margin:2em 0;border:1px solid var(--un-prose-hr);}.prose :where(table):not(:where(.not-prose,.not-prose *)){display:block;margin:1em 0;border-collapse:collapse;overflow-x:auto;}.prose :where(tr):not(:where(.not-prose,.not-prose *)):nth-child(2n){background:var(--un-prose-bg-soft);}.prose :where(td,th):not(:where(.not-prose,.not-prose *)){border:1px solid var(--un-prose-borders);padding:.625em 1em;}.prose :where(abbr):not(:where(.not-prose,.not-prose *)){cursor:help;}.prose :where(kbd):not(:where(.not-prose,.not-prose *)){color:var(--un-prose-code);border:1px solid;padding:.25rem .5rem;font-size:.875em;border-radius:.25rem;}.prose :where(details):not(:where(.not-prose,.not-prose *)){margin:1em 0;padding:1.25rem 1.5rem;background:var(--un-prose-bg-soft);}.prose :where(summary):not(:where(.not-prose,.not-prose *)){cursor:pointer;font-weight:600;}.prose :where(code):not(:where(.not-prose,.not-prose *))::before{content:"" !important;}.prose :where(code):not(:where(.not-prose,.not-prose *))::after{content:"" !important;}
|
||||
.prose{color:var(--un-prose-body);max-width:65ch;}
|
||||
/* layer: shortcuts */
|
||||
.container{width:100%;}
|
||||
@media (min-width: 640px){
|
||||
.container{max-width:640px;}
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
.container{max-width:768px;}
|
||||
}
|
||||
@media (min-width: 1024px){
|
||||
.container{max-width:1024px;}
|
||||
}
|
||||
@media (min-width: 1280px){
|
||||
.container{max-width:1280px;}
|
||||
}
|
||||
@media (min-width: 1536px){
|
||||
.container{max-width:1536px;}
|
||||
}
|
||||
/* layer: default */
|
||||
.fixed{position:fixed;}
|
||||
.relative{position:relative;}
|
||||
.inset-0{inset:0;}
|
||||
.z-10{z-index:10;}
|
||||
.z-30{z-index:30;}
|
||||
.z-40{z-index:40;}
|
||||
.grid{display:grid;}
|
||||
.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr;}
|
||||
.mb-2{margin-bottom:0.5rem;}
|
||||
.mb-4{margin-bottom:1rem;}
|
||||
.me{margin-inline-end:1rem;}
|
||||
.inline{display:inline;}
|
||||
.block{display:block;}
|
||||
.inline-block{display:inline-block;}
|
||||
.contents{display:contents;}
|
||||
.hidden{display:none;}
|
||||
.h-8{height:2rem;}
|
||||
.h-full{height:100%;}
|
||||
.h-screen{height:100vh;}
|
||||
.max-w-sm{max-width:24rem;}
|
||||
.min-h-screen{min-height:100vh;}
|
||||
.min-w-52{min-width:13rem;}
|
||||
.min-w-64{min-width:16rem;}
|
||||
.w-24{width:6rem;}
|
||||
.w-64{width:16rem;}
|
||||
.w-full{width:100%;}
|
||||
.flex{display:flex;}
|
||||
.inline-flex{display:inline-flex;}
|
||||
.flex-1{flex:1 1 0%;}
|
||||
.flex-col{flex-direction:column;}
|
||||
.flex-wrap{flex-wrap:wrap;}
|
||||
.table{display:table;}
|
||||
.transform{transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
|
||||
.cursor-pointer{cursor:pointer;}
|
||||
.scroll-mb-16{scroll-margin-bottom:4rem;}
|
||||
.list-disc{list-style-type:disc;}
|
||||
.list-none{list-style-type:none;}
|
||||
.appearance-none{-webkit-appearance:none;appearance:none;}
|
||||
.items-end{align-items:flex-end;}
|
||||
.items-center{align-items:center;}
|
||||
.justify-start{justify-content:flex-start;}
|
||||
.justify-end{justify-content:flex-end;}
|
||||
.justify-center{justify-content:center;}
|
||||
.justify-between{justify-content:space-between;}
|
||||
.gap-1{gap:0.25rem;}
|
||||
.gap-2{gap:0.5rem;}
|
||||
.gap-3{gap:0.75rem;}
|
||||
.gap-4{gap:1rem;}
|
||||
.gap-6{gap:1.5rem;}
|
||||
.gap-8{gap:2rem;}
|
||||
.overflow-hidden{overflow:hidden;}
|
||||
.overflow-y-auto{overflow-y:auto;}
|
||||
.overflow-y-scroll{overflow-y:scroll;}
|
||||
.text-ellipsis{text-overflow:ellipsis;}
|
||||
.b,
|
||||
.border{border-width:1px;}
|
||||
.border-2{border-width:2px;}
|
||||
.border-b{border-bottom-width:1px;}
|
||||
.border-b-2{border-bottom-width:2px;}
|
||||
.border-l{border-left-width:1px;}
|
||||
.border-l-2{border-left-width:2px;}
|
||||
.border-r{border-right-width:1px;}
|
||||
.border-t{border-top-width:1px;}
|
||||
.border-accent-500{--un-border-opacity:1;border-color:rgb(55 97 187 / var(--un-border-opacity));}
|
||||
.border-accent-600{--un-border-opacity:1;border-color:rgb(44 78 150 / var(--un-border-opacity));}
|
||||
.border-accent-700{--un-border-opacity:1;border-color:rgb(33 58 112 / var(--un-border-opacity));}
|
||||
.border-primary-300{--un-border-opacity:1;border-color:rgb(214 189 135 / var(--un-border-opacity));}
|
||||
.border-primary-400{--un-border-opacity:1;border-color:rgb(201 167 95 / var(--un-border-opacity));}
|
||||
.border-primary-500{--un-border-opacity:1;border-color:rgb(187 145 55 / var(--un-border-opacity));}
|
||||
.focus\:border-accent-400:focus{--un-border-opacity:1;border-color:rgb(95 129 201 / var(--un-border-opacity));}
|
||||
.focus\:border-primary-400:focus{--un-border-opacity:1;border-color:rgb(201 167 95 / var(--un-border-opacity));}
|
||||
.rounded{border-radius:0.25rem;}
|
||||
.rounded-full{border-radius:9999px;}
|
||||
.rounded-lg{border-radius:0.5rem;}
|
||||
.rounded-md{border-radius:0.375rem;}
|
||||
.rounded-sm{border-radius:0.125rem;}
|
||||
.rounded-l{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;}
|
||||
.rounded-r{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;}
|
||||
.rounded-t{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem;}
|
||||
.border-dashed{border-style:dashed;}
|
||||
.bg-accent-300{--un-bg-opacity:1;background-color:rgb(135 160 214 / var(--un-bg-opacity));}
|
||||
.bg-accent-500{--un-bg-opacity:1;background-color:rgb(55 97 187 / var(--un-bg-opacity));}
|
||||
.bg-accent-600{--un-bg-opacity:1;background-color:rgb(44 78 150 / var(--un-bg-opacity));}
|
||||
.bg-accent-700{--un-bg-opacity:1;background-color:rgb(33 58 112 / var(--un-bg-opacity));}
|
||||
.bg-accent-800{--un-bg-opacity:1;background-color:rgb(22 39 75 / var(--un-bg-opacity));}
|
||||
.bg-accent-900{--un-bg-opacity:1;background-color:rgb(11 19 37 / var(--un-bg-opacity));}
|
||||
.bg-error-500{--un-bg-opacity:1;background-color:rgb(195 105 105 / var(--un-bg-opacity));}
|
||||
.bg-error-600{--un-bg-opacity:1;background-color:rgb(159 84 84 / var(--un-bg-opacity));}
|
||||
.bg-error-700{--un-bg-opacity:1;background-color:rgb(123 63 63 / var(--un-bg-opacity));}
|
||||
.bg-primary-300{--un-bg-opacity:1;background-color:rgb(214 189 135 / var(--un-bg-opacity));}
|
||||
.bg-primary-400{--un-bg-opacity:1;background-color:rgb(201 167 95 / var(--un-bg-opacity));}
|
||||
.bg-primary-600{--un-bg-opacity:1;background-color:rgb(150 116 44 / var(--un-bg-opacity));}
|
||||
.bg-primary-700{--un-bg-opacity:1;background-color:rgb(112 87 33 / var(--un-bg-opacity));}
|
||||
.bg-primary-900{--un-bg-opacity:1;background-color:rgb(37 29 11 / var(--un-bg-opacity));}
|
||||
.bg-red-500{--un-bg-opacity:1;background-color:rgb(239 68 68 / var(--un-bg-opacity));}
|
||||
.bg-success-300{--un-bg-opacity:1;background-color:rgb(165 219 187 / var(--un-bg-opacity));}
|
||||
.bg-success-500{--un-bg-opacity:1;background-color:rgb(105 195 131 / var(--un-bg-opacity));}
|
||||
.bg-success-600{--un-bg-opacity:1;background-color:rgb(84 159 106 / var(--un-bg-opacity));}
|
||||
.bg-success-700{--un-bg-opacity:1;background-color:rgb(63 123 81 / var(--un-bg-opacity));}
|
||||
.hover\:bg-accent-400:hover{--un-bg-opacity:1;background-color:rgb(95 129 201 / var(--un-bg-opacity));}
|
||||
.hover\:bg-accent-500:hover{--un-bg-opacity:1;background-color:rgb(55 97 187 / var(--un-bg-opacity));}
|
||||
.hover\:bg-accent-700:hover{--un-bg-opacity:1;background-color:rgb(33 58 112 / var(--un-bg-opacity));}
|
||||
.hover\:bg-error-500:hover{--un-bg-opacity:1;background-color:rgb(195 105 105 / var(--un-bg-opacity));}
|
||||
.hover\:bg-error-600:hover{--un-bg-opacity:1;background-color:rgb(159 84 84 / var(--un-bg-opacity));}
|
||||
.hover\:bg-primary-400:hover{--un-bg-opacity:1;background-color:rgb(201 167 95 / var(--un-bg-opacity));}
|
||||
.hover\:bg-primary-500:hover{--un-bg-opacity:1;background-color:rgb(187 145 55 / var(--un-bg-opacity));}
|
||||
.hover\:bg-primary-700:hover{--un-bg-opacity:1;background-color:rgb(112 87 33 / var(--un-bg-opacity));}
|
||||
.hover\:bg-success-400:hover{--un-bg-opacity:1;background-color:rgb(135 207 159 / var(--un-bg-opacity));}
|
||||
.hover\:bg-success-500:hover{--un-bg-opacity:1;background-color:rgb(105 195 131 / var(--un-bg-opacity));}
|
||||
.hover\:bg-success-600:hover{--un-bg-opacity:1;background-color:rgb(84 159 106 / var(--un-bg-opacity));}
|
||||
.bg-opacity-70{--un-bg-opacity:0.7;}
|
||||
.from-accent-700{--un-gradient-from-position:0%;--un-gradient-from:rgb(33 58 112 / var(--un-from-opacity, 1)) var(--un-gradient-from-position);--un-gradient-to-position:100%;--un-gradient-to:rgb(33 58 112 / 0) var(--un-gradient-to-position);--un-gradient-stops:var(--un-gradient-from), var(--un-gradient-to);}
|
||||
.from-primary-800{--un-gradient-from-position:0%;--un-gradient-from:rgb(75 58 22 / var(--un-from-opacity, 1)) var(--un-gradient-from-position);--un-gradient-to-position:100%;--un-gradient-to:rgb(75 58 22 / 0) var(--un-gradient-to-position);--un-gradient-stops:var(--un-gradient-from), var(--un-gradient-to);}
|
||||
.to-accent-900{--un-gradient-to-position:100%;--un-gradient-to:rgb(11 19 37 / var(--un-to-opacity, 1)) var(--un-gradient-to-position);}
|
||||
.to-primary-900{--un-gradient-to-position:100%;--un-gradient-to:rgb(37 29 11 / var(--un-to-opacity, 1)) var(--un-gradient-to-position);}
|
||||
.bg-gradient-to-br{--un-gradient-shape:to bottom right;--un-gradient:var(--un-gradient-shape), var(--un-gradient-stops);background-image:linear-gradient(var(--un-gradient));}
|
||||
.fill-current{fill:currentColor;}
|
||||
.p-1{padding:0.25rem;}
|
||||
.p-2{padding:0.5rem;}
|
||||
.p-2\.5{padding:0.625rem;}
|
||||
.p-4{padding:1rem;}
|
||||
.p-8{padding:2rem;}
|
||||
.px-2{padding-left:0.5rem;padding-right:0.5rem;}
|
||||
.px-3{padding-left:0.75rem;padding-right:0.75rem;}
|
||||
.px-4{padding-left:1rem;padding-right:1rem;}
|
||||
.py,
|
||||
.py-4{padding-top:1rem;padding-bottom:1rem;}
|
||||
.py-1{padding-top:0.25rem;padding-bottom:0.25rem;}
|
||||
.py-2{padding-top:0.5rem;padding-bottom:0.5rem;}
|
||||
.py-8{padding-top:2rem;padding-bottom:2rem;}
|
||||
.py-px{padding-top:1px;padding-bottom:1px;}
|
||||
.pl-4{padding-left:1rem;}
|
||||
.text-center{text-align:center;}
|
||||
.text-right{text-align:right;}
|
||||
.text-2xl{font-size:1.5rem;line-height:2rem;}
|
||||
.text-3xl{font-size:1.875rem;line-height:2.25rem;}
|
||||
.text-4xl{font-size:2.25rem;line-height:2.5rem;}
|
||||
.text-5xl{font-size:3rem;line-height:1;}
|
||||
.text-base{font-size:1rem;line-height:1.5rem;}
|
||||
.text-lg{font-size:1.125rem;line-height:1.75rem;}
|
||||
.text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||
.text-xl{font-size:1.25rem;line-height:1.75rem;}
|
||||
.text-xs{font-size:0.75rem;line-height:1rem;}
|
||||
.text-accent-100{--un-text-opacity:1;color:rgb(215 223 241 / var(--un-text-opacity));}
|
||||
.text-accent-200{--un-text-opacity:1;color:rgb(175 192 228 / var(--un-text-opacity));}
|
||||
.text-accent-300{--un-text-opacity:1;color:rgb(135 160 214 / var(--un-text-opacity));}
|
||||
.text-accent-50{--un-text-opacity:1;color:rgb(235 239 248 / var(--un-text-opacity));}
|
||||
.text-accent-800{--un-text-opacity:1;color:rgb(22 39 75 / var(--un-text-opacity));}
|
||||
.text-error-100{--un-text-opacity:1;color:rgb(243 225 225 / var(--un-text-opacity));}
|
||||
.text-error-400{--un-text-opacity:1;color:rgb(207 135 135 / var(--un-text-opacity));}
|
||||
.text-primary-100{--un-text-opacity:1;color:rgb(241 233 215 / var(--un-text-opacity));}
|
||||
.text-primary-200{--un-text-opacity:1;color:rgb(228 211 175 / var(--un-text-opacity));}
|
||||
.text-primary-300{--un-text-opacity:1;color:rgb(214 189 135 / var(--un-text-opacity));}
|
||||
.text-primary-50{--un-text-opacity:1;color:rgb(248 244 235 / var(--un-text-opacity));}
|
||||
.text-primary-800{--un-text-opacity:1;color:rgb(75 58 22 / var(--un-text-opacity));}
|
||||
.text-success-100{--un-text-opacity:1;color:rgb(225 243 233 / var(--un-text-opacity));}
|
||||
.text-success-800{--un-text-opacity:1;color:rgb(42 87 56 / var(--un-text-opacity));}
|
||||
.text-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));}
|
||||
.hover\:text-accent-100:hover{--un-text-opacity:1;color:rgb(215 223 241 / var(--un-text-opacity));}
|
||||
.hover\:text-primary-200:hover{--un-text-opacity:1;color:rgb(228 211 175 / var(--un-text-opacity));}
|
||||
.font-bold{font-weight:700;}
|
||||
.font-light{font-weight:300;}
|
||||
.font-medium{font-weight:500;}
|
||||
.leading-5{line-height:1.25rem;}
|
||||
.leading-tight{line-height:1.25;}
|
||||
.font-brand{font-family:"Orbitron";}
|
||||
.font-mono{font-family:"Fira Code",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;}
|
||||
.font-sans{font-family:"Inter",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";}
|
||||
.uppercase{text-transform:uppercase;}
|
||||
.underline{text-decoration-line:underline;}
|
||||
.decoration-primary-300{-webkit-text-decoration-color:rgb(214 189 135 / var(--un-line-opacity));--un-line-opacity:1;text-decoration-color:rgb(214 189 135 / var(--un-line-opacity));}
|
||||
.no-underline{text-decoration:none;}
|
||||
.opacity-0{opacity:0;}
|
||||
.opacity-100{opacity:1;}
|
||||
.shadow{--un-shadow:var(--un-shadow-inset) 0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||
.shadow-lg{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||
.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px;}
|
||||
.focus\:ring-accent-400:focus{--un-ring-opacity:1;--un-ring-color:rgb(95 129 201 / var(--un-ring-opacity));}
|
||||
.focus\:ring-primary-400:focus{--un-ring-opacity:1;--un-ring-color:rgb(201 167 95 / var(--un-ring-opacity));}
|
||||
.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
||||
.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
||||
.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
||||
.duration-\[2000ms\]{transition-duration:2000ms;}
|
||||
.duration-1000{transition-duration:1000ms;}
|
||||
.un-cloak{display:block !important;}
|
||||
@media (min-width: 768px){
|
||||
.md\:visible{visibility:visible;}
|
||||
.md\:block{display:block;}
|
||||
.md\:hidden{display:none;}
|
||||
.md\:h-64{height:16rem;}
|
||||
.md\:max-w-2xl{max-width:42rem;}
|
||||
.md\:max-w-md{max-width:28rem;}
|
||||
.md\:w-64{width:16rem;}
|
||||
.md\:flex{display:flex;}
|
||||
.md\:items-center{align-items:center;}
|
||||
.md\:justify-center{justify-content:center;}
|
||||
.md\:p-4{padding:1rem;}
|
||||
.md\:p-8{padding:2rem;}
|
||||
.md\:px-4{padding-left:1rem;padding-right:1rem;}
|
||||
.md\:text-center{text-align:center;}
|
||||
.md\:text-6xl{font-size:3.75rem;line-height:1;}
|
||||
.md\:text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||
}
|
||||
@media (min-width: 1024px){
|
||||
.lg\:block{display:block;}
|
||||
.lg\:text-lg{font-size:1.125rem;line-height:1.75rem;}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dcb2b1820e15ee93b35c4825ef62649cd3ae1978b4db33ffcc61c1dab85d1f9
|
||||
size 129778
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e7d99edcb59f0299ff4ed5ad00baefe0b4e937008c7a9a297bc832654b04a8b7
|
||||
size 22011
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:694e57fbc74689f5929069de2d8dcd311c052e30b488547fe34e5fc01ed90b0d
|
||||
size 51314
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ded8c96be2a77738ddbd2f43b9d6c49e2e4c40756c8fb12ee2a60d64d4a1a0ec
|
||||
size 34557
|
|
@ -1,187 +0,0 @@
|
|||
export declare type Action = (ctx: AttributeContext, ...args: string[]) => Promise<any>;
|
||||
|
||||
export declare type Actions = Record<string, Action>;
|
||||
|
||||
declare type AtomicState = Array<unknown> | ((...args: unknown[]) => unknown) | string | boolean | number | bigint | symbol | undefined | null;
|
||||
|
||||
export declare type AttributeContext = {
|
||||
store: () => any;
|
||||
mergeStore: (store: DeepState) => void;
|
||||
applyPlugins: (target: Element) => void;
|
||||
walkSignals: (cb: (name: string, signal: Signal<any>) => void) => void;
|
||||
cleanupElementRemovals: (el: Element) => void;
|
||||
actions: Readonly<Actions>;
|
||||
refs: Record<string, HTMLorSVGElement>;
|
||||
reactivity: Reactivity;
|
||||
el: Readonly<HTMLorSVGElement>;
|
||||
key: Readonly<string>;
|
||||
expression: Readonly<string>;
|
||||
expressionFn: ExpressionFunction;
|
||||
modifiers: Map<string, string[]>;
|
||||
};
|
||||
|
||||
export declare type AttributePlugin = {
|
||||
prefix: string;
|
||||
requiredPluginPrefixes?: Iterable<string>;
|
||||
onGlobalInit?: (ctx: InitContext) => void;
|
||||
onLoad: (ctx: AttributeContext) => OnRemovalFn | void;
|
||||
allowedModifiers?: Set<string>;
|
||||
mustHaveEmptyExpression?: boolean;
|
||||
mustNotEmptyExpression?: boolean;
|
||||
mustHaveEmptyKey?: boolean;
|
||||
mustNotEmptyKey?: boolean;
|
||||
allowedTagRegexps?: Set<string>;
|
||||
disallowedTags?: Set<string>;
|
||||
preprocessors?: {
|
||||
pre?: Preprocessor[];
|
||||
post?: Preprocessor[];
|
||||
};
|
||||
bypassExpressionFunctionCreation?: (ctx: AttributeContext) => boolean;
|
||||
};
|
||||
|
||||
declare function Computed(this: Computed, compute: () => unknown): void;
|
||||
|
||||
declare class Computed<T = any> extends Signal<T> {
|
||||
_compute: () => T;
|
||||
_sources?: Node_2;
|
||||
_globalVersion: number;
|
||||
_flags: number;
|
||||
constructor(compute: () => T);
|
||||
_notify(): void;
|
||||
get value(): T;
|
||||
}
|
||||
|
||||
declare namespace Computed {
|
||||
var prototype: Computed<any>;
|
||||
}
|
||||
|
||||
export declare class Datastar {
|
||||
plugins: AttributePlugin[];
|
||||
store: DeepSignal<any>;
|
||||
actions: Actions;
|
||||
refs: Record<string, HTMLElement>;
|
||||
reactivity: Reactivity;
|
||||
parentID: string;
|
||||
missingIDNext: number;
|
||||
removals: Map<Element, Set<OnRemovalFn>>;
|
||||
constructor(actions?: Actions, ...plugins: AttributePlugin[]);
|
||||
run(): void;
|
||||
private cleanupElementRemovals;
|
||||
private mergeStore;
|
||||
signalByName<T>(name: string): Signal<T>;
|
||||
private applyPlugins;
|
||||
private walkSignalsStore;
|
||||
private walkSignals;
|
||||
private walkDownDOM;
|
||||
}
|
||||
|
||||
export declare type DatastarPlugin = {};
|
||||
|
||||
declare class DeepSignal<T extends DeepState> implements DeepSignalAccessors<T> {
|
||||
get value(): ReadOnlyDeep<T>;
|
||||
set value(payload: ReadOnlyDeep<T>);
|
||||
peek(): ReadOnlyDeep<T>;
|
||||
}
|
||||
|
||||
declare interface DeepSignalAccessors<T extends DeepState> {
|
||||
value: ReadOnlyDeep<T>;
|
||||
peek: () => ReadOnlyDeep<T>;
|
||||
}
|
||||
|
||||
declare type DeepState = {
|
||||
[key: string]: (() => unknown) | AtomicState | DeepState;
|
||||
};
|
||||
|
||||
declare function Effect(this: Effect, compute: () => unknown | EffectCleanup): void;
|
||||
|
||||
declare class Effect {
|
||||
_compute?: () => unknown | EffectCleanup;
|
||||
_cleanup?: () => unknown;
|
||||
_sources?: Node_2;
|
||||
_nextBatchedEffect?: Effect;
|
||||
_flags: number;
|
||||
constructor(compute: () => unknown | EffectCleanup);
|
||||
_callback(): void;
|
||||
_start(): () => void;
|
||||
_notify(): void;
|
||||
_dispose(): void;
|
||||
}
|
||||
|
||||
declare type EffectCleanup = () => unknown;
|
||||
|
||||
export declare type ExpressionFunction = (ctx: AttributeContext) => any;
|
||||
|
||||
export declare type HTMLorSVGElement = Element & (HTMLElement | SVGElement);
|
||||
|
||||
declare const identifier: unique symbol;
|
||||
|
||||
export declare type InitContext = {
|
||||
store: any;
|
||||
mergeStore: (store: DeepState) => void;
|
||||
actions: Readonly<Actions>;
|
||||
refs: Record<string, HTMLorSVGElement>;
|
||||
reactivity: Reactivity;
|
||||
};
|
||||
|
||||
declare type Node_2 = {
|
||||
_source: Signal;
|
||||
_prevSource?: Node_2;
|
||||
_nextSource?: Node_2;
|
||||
_target: Computed | Effect;
|
||||
_prevTarget?: Node_2;
|
||||
_nextTarget?: Node_2;
|
||||
_version: number;
|
||||
_rollbackNode?: Node_2;
|
||||
};
|
||||
|
||||
export declare type OnRemovalFn = () => void;
|
||||
|
||||
export declare type Preprocessor = {
|
||||
regexp: RegExp;
|
||||
replacer: (groups: RegexpGroups) => string;
|
||||
};
|
||||
|
||||
export declare type Reactivity = {
|
||||
signal: <T>(value: T) => Signal<T>;
|
||||
computed: <T>(fn: () => T) => ReadonlySignal<T>;
|
||||
effect: (cb: () => void) => OnRemovalFn;
|
||||
};
|
||||
|
||||
declare type ReadOnlyDeep<T> = {
|
||||
readonly [P in keyof T]: ReadOnlyDeep<T[P]>;
|
||||
};
|
||||
|
||||
declare interface ReadonlySignal<T = any> extends Signal<T> {
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
export declare type RegexpGroups = Record<string, string>;
|
||||
|
||||
export declare function runDatastarWith(actions?: Actions, ...plugins: AttributePlugin[]): Datastar;
|
||||
|
||||
export declare function runDatastarWithAllPlugins(addedActions?: Actions, ...addedPlugins: AttributePlugin[]): Datastar;
|
||||
|
||||
/* Excluded declaration from this release type: Signal */
|
||||
|
||||
declare class Signal<T = any> {
|
||||
/* Excluded from this release type: _value */
|
||||
/* Excluded from this release type: _version */
|
||||
/* Excluded from this release type: _node */
|
||||
/* Excluded from this release type: _targets */
|
||||
constructor(value?: T);
|
||||
/* Excluded from this release type: _refresh */
|
||||
/* Excluded from this release type: _subscribe */
|
||||
/* Excluded from this release type: _unsubscribe */
|
||||
subscribe(fn: (value: T) => void): () => void;
|
||||
valueOf(): T;
|
||||
toString(): string;
|
||||
toJSON(): T;
|
||||
peek(): T;
|
||||
brand: typeof identifier;
|
||||
get value(): T;
|
||||
set value(value: T);
|
||||
}
|
||||
|
||||
export declare function toHTMLorSVGElement(node: Node): HTMLorSVGElement | null;
|
||||
|
||||
export { }
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"name": "@sudodevnull/datastar",
|
||||
"version": "0.12.7",
|
||||
"type": "module",
|
||||
"homepage": "https://data-star.dev",
|
||||
"repository": "https://github.com/delaneyj/datastar",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"private": false,
|
||||
"main": "dist/datastar.js",
|
||||
"module": "dist/datastar.js",
|
||||
"types": "dist/datastar.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/datastar.js",
|
||||
"require": "./dist/datastar.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"@unocss/cli": "^0.59.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-attributes": "^1.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.4.3",
|
||||
"unocss": "^0.58.6",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-dts": "^3.7.3"
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
# Batteries Included
|
||||
|
||||
## Build modular, think globally
|
||||
|
||||
Datastar was originally a branch of HTMX that was meant to make it more modular and easier to write plugins for. What was not expected is in creating what felt like essential plugins was actually able to recreate all the needed features of a full SPA frameworks, HTMX and Alpine and do it all in smaller package. While you can still build your own plugins, the core set of plugins is enough to build a full application. Let's explore some more features
|
||||
|
||||
## Evaluating data-\* attributes
|
||||
|
||||
Before we had used something like `data-text="$foo"` attribute. What this is actually doing under the hood is turning the string into a function that is evaluated safely into `(ctx)=> ctx.store.foo.value` where `ctx` is the Datastar object that was injected into the page. In this case its connected directly to the contents of a signal but you could do any javascript that is valid. This also means you could eval to a constant string but that would looks something like `data-text="'hello world'"`, notice the quotes, otherwise it would be looking for variable `hello world` which isn't valid and would fail to evaluate. This is the one major gotcha of declarative code, you have to be careful about the context.
|
||||
|
||||
## More attributes
|
||||
|
||||
So far we've seen `data-store`, `data-model` and `data-text`. There are many more attributes that can be used to make your page more reactive. Check out the API for a full list of attributes but here are a few more that get used the most.
|
||||
|
||||
### `data-show`
|
||||
|
||||
This is a simple way to show and hide elements. It's a simple way to do `v-if` in Vue or `x-show` in Alpine.
|
||||
|
||||
```html
|
||||
<div data-show="$foo">Hello World</div>
|
||||
```
|
||||
|
||||
When the `foo` signal is truthy the div will be shown, otherwise it will be hidden.
|
||||
|
||||
### `data-on`
|
||||
|
||||
This is a way to add event listeners to elements. It's a simple way to do `v-on` in Vue or `x-on` in Alpine.
|
||||
|
||||
```html
|
||||
<button data-on-click="console.log('hello world')">Click me</button>
|
||||
```
|
||||
|
||||
When the button is clicked it will log `hello world` to the console.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are helper functions that are made available during a data-\* attribute evaluation. They are a way to do more complex operations without having to write a lot of javascript. Here are a few of the most common actions. They are prefixed with `$$` to avoid any conflicts with other attributes.
|
||||
|
||||
### `$$setAll`
|
||||
|
||||
Sometimes you want to set multiple values at once. It takes a prefix regexp and a set of signals. For example if you have a form with a bunch of inputs and you want to set them all at once you could do something like this.
|
||||
|
||||
```html
|
||||
data-text="$$setAll('contact_',true)`
|
||||
```
|
||||
|
||||
### `$$toggleAll`
|
||||
|
||||
Works the same as `$$setAll` but will flip the value of the signals.
|
||||
|
||||
There can be many more actions and you can even write your own. But the majority of the time you'll be using the actions that allow for backend integration, which we'll cover in the next section.
|
|
@ -1,359 +0,0 @@
|
|||
# Getting Started
|
||||
|
||||
## A Birds Eye View
|
||||
|
||||
If you are familiar with libraries like [HTMX](https://htmx.org/) or [AlpineJs](https://alpinejs.dev/); Datastar brings them together. This breaks down essentially to:
|
||||
|
||||
1. Send the current UI from your backend via HTML fragments like HTMX.
|
||||
2. Manage client side state that wouldn't make sense to be managed by your backend like AlpineJS.
|
||||
|
||||
I've had [thoughts](/essays/why_another_framework) on both of these in the past. TLDR; While both libraries are great, I wanted to go in a different direction.
|
||||
|
||||
Datastar accomplishes both tasks in a unified manner and it's [tiny](https://bundlephobia.com/package/@sudodevnull/datastar).
|
||||
|
||||
## Installation
|
||||
|
||||
To get started you must first get a copy of Datastar. There are a few ways to do this.
|
||||
|
||||
**Remotely**
|
||||
|
||||
You can include it directly into your html using a script tag:
|
||||
|
||||
```html
|
||||
<script
|
||||
type="module"
|
||||
defer
|
||||
src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"
|
||||
></script>
|
||||
```
|
||||
|
||||
**NPM**
|
||||
|
||||
For npm-style build systems, you can install Datastar via npm and then import this in your server file.
|
||||
|
||||
```bash
|
||||
npm i @sudodevnull/datastar
|
||||
```
|
||||
|
||||
## A Quick Primer
|
||||
|
||||
Now let's get our feet wet. We'll walk through some ways to use Datastar with a quick example. For our example we'll just spin up an [Express](https://expressjs.com/en/starter/hello-world.html) server on Node. We'll have the server prepare a template for us when we first navigate to it.
|
||||
|
||||
You can copy the code below to get started. Don't worry, we've already installed Datastar for you using a [CDN](#remotely).
|
||||
|
||||
```js
|
||||
const express = require("express");
|
||||
const { randomBytes } = require("crypto");
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
const backendData = {};
|
||||
|
||||
function indexPage() {
|
||||
const indexPage = `
|
||||
<!doctype html><html>
|
||||
<head>
|
||||
<title>Node/Express + Datastar Example</title>
|
||||
<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script></head>
|
||||
<body>
|
||||
<h2>Node/Express + Datastar Example</h2>
|
||||
<main class="container" id="main"></main>
|
||||
</body>
|
||||
</html>`;
|
||||
return indexPage;
|
||||
}
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send(indexPage()).end();
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
We have basic server set up. Now let's get to the fun part. Let's add some Datastar functionality.
|
||||
|
||||
## Handling State
|
||||
|
||||
Let's start out with how Datastar handles state. Enter the [store](/reference/plugins_core#merge-store) attribute.
|
||||
|
||||
Go ahead and add this as a data attribute to the `<main>` element:
|
||||
|
||||
```html
|
||||
<main class="container" id="main" data-store='{ "input": "" }'></main>
|
||||
```
|
||||
|
||||
This is the global store, if you make multiple stores they will actually merge into one store behind the scenes. If there are two fields with the same name, Datastar will resolve as last in wins.
|
||||
|
||||
The store is great and all but how can we use it? There are many ways. Let's check some of them out.
|
||||
|
||||
## Some Reactivity
|
||||
|
||||
If you had a keen eye, you noticed we put `input` as a field on our store. What's that about? Glad you asked! It's Datastar's way to sets up two-way data binding on an element. In our case this `input` element. Say hi to the [Model](/reference/plugins_attributes#model) attribute.
|
||||
|
||||
Stick this inside of your `<main>` element:
|
||||
|
||||
```html
|
||||
<input type="text" placeholder="Type here!" data-model="input" />
|
||||
```
|
||||
|
||||
This binds to a signal so our store can stay up to date with whatever is typed into this input. You can even nest your state like this `{"nested":{"label":"foo"}}` and use `data-model="nested.label"` or access it from the backend as needed.
|
||||
|
||||
Good stuff so far. How can we see this? We can check the changes locally using the [data-text](/reference/plugins_attributes#text) attribute.
|
||||
|
||||
Create a div in your `<main>` Element:
|
||||
|
||||
```html
|
||||
<div data-text="$input"></div>
|
||||
```
|
||||
|
||||
Sets the text content of an element to the value of the signal. Now check it out, client-side reactivity! We can have different types of state as well. We can even do fun stuff like `data-text="$value.toUpperCase()"`.
|
||||
|
||||
Speaking of which, let's do some more! Let's play hide 'n seek with the [data-show](/reference/plugins_visibility#show) attribute.
|
||||
|
||||
Add this to your store:
|
||||
|
||||
```js
|
||||
{ input: "", show: false };
|
||||
```
|
||||
|
||||
We can hide elements and show them without using JavaScript! How will we trigger this though?
|
||||
|
||||
## Events
|
||||
|
||||
We bring in the [On](/reference/plugins_attributes#on) attribute. This sets up an event listener on an element. In this example, we're using `data-on-click`. You will later see there are other `data-on` actions we can utilize. You can also do silly things like `data-on-click="console.log('hello world')"`.
|
||||
|
||||
Add this inside of your `<main>` element:
|
||||
|
||||
```html
|
||||
<button data-on-click="$show=!$show">Toggle</button>
|
||||
<div data-show="$show">
|
||||
<span>Hello From Datastar!</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
So what else can we do? You can mess around and do some fun stuff with expressions. For instance, something like: `"$prompt=prompt('Enter something',$prompt);$confirm=confirm('Sure?');$confirm && $$get('/sure')"` is totally feasible.
|
||||
|
||||
Anyhow, we haven't really even scratched the surface. Let's keep going.
|
||||
|
||||
## Backend Plumbing
|
||||
|
||||
Now, let's send some data. To do this there's a few things we must understand but it's all fun and easy and you'll want to know it if you do not already!
|
||||
|
||||
Datastar uses [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) or SSE. To use SSE, we have to set our backend up for it. Luckily it's extremely simple and [provides us with many advantages](/essays/event_streams_all_the_way_down).
|
||||
|
||||
Let's set things up. Copy the below code to your server.
|
||||
|
||||
Copy this to your server code:
|
||||
|
||||
```js
|
||||
function setHeaders(res) {
|
||||
res.set({
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/event-stream",
|
||||
Connection: "keep-alive",
|
||||
});
|
||||
res.flushHeaders();
|
||||
}
|
||||
```
|
||||
|
||||
`setHeaders` is simple a utility function we will use on our endpoints to set our headers to use SSE.
|
||||
|
||||
Copy this to your server code:
|
||||
|
||||
```js
|
||||
function sendSSE({ res, frag, selector, merge, mergeType, end }) {
|
||||
res.write("event: datastar-fragment\n");
|
||||
if (selector) res.write(`data: selector ${selector}\n`);
|
||||
if (merge) res.write(`data: merge ${mergeType}\n`);
|
||||
res.write(`data: fragment ${frag}\n\n`);
|
||||
if (end) res.end();
|
||||
}
|
||||
```
|
||||
|
||||
We will use `sendSSE` as another utility function that will help us configure our response to fit SSE and Datastar formats. Let's check that out real quick.
|
||||
|
||||
## Stay In Formation
|
||||
|
||||
SSE messages are text-based and consist of one or more "events". Each event is separated by a pair (`\n\n`) of newline characters. An individual event consists of one or more lines of text, each followed by a newline character (`\n)`), and uses a simple key-value pair [format](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format).
|
||||
|
||||
For our Datastar example:
|
||||
|
||||
```
|
||||
event: datastar-fragment // \n
|
||||
id: 129618219840307262 // \n
|
||||
data: merge morph_element // \n
|
||||
data: fragment <div id="id">...</div> // \n\n
|
||||
|
||||
```
|
||||
|
||||
**NOTE** in the real message the comments and newlines wouldn't be visible
|
||||
|
||||
Each data message in the event is separated by a (`\n`) newline and each event is separated by a pair of (`\n\n`) newlines. If you notice that is what we are doing in `sendSSE` except with a little flavor added so we can tell Datastar what we want to do using [Datastar's format](/reference/plugins_backend#datastar-sse-event).
|
||||
|
||||
Now let's make our route.
|
||||
|
||||
Copy this to your server code:
|
||||
|
||||
```js
|
||||
app.put("/put", (req, res) => {
|
||||
setHeaders(res);
|
||||
const { input } = req.body;
|
||||
backendData.input = input;
|
||||
const output = `Your input: ${input}, is ${input.length} long.`;
|
||||
let frag = `<div id="output">${output}</div>`;
|
||||
sendSSE({
|
||||
res,
|
||||
frag,
|
||||
selector: null,
|
||||
merge: true,
|
||||
mergeType: "morph_element",
|
||||
end: true,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
So here you see we're setting our headers with `setHeaders`. We modify state that's stored specifically on the backend. This can be anything you want, like a database. Then we construct the response string much like HTMX and include the store attribute. We send the response with the `morph_element` merge type.
|
||||
|
||||
We need to make some changes now to reflect this.
|
||||
|
||||
Go ahead and modify your html.
|
||||
|
||||
Change this in your `<main>` element:
|
||||
|
||||
```html
|
||||
<div data-text="$input"></div>
|
||||
```
|
||||
|
||||
To this:
|
||||
|
||||
```html
|
||||
<div id="output"></div>
|
||||
```
|
||||
|
||||
Give ourselves a button to perform this action.
|
||||
|
||||
Add this to your `<main>` element:
|
||||
|
||||
```html
|
||||
<button data-on-click="$$put('/put')">Send State</button>
|
||||
```
|
||||
|
||||
...and give ourselves a place to show our new state on the client.
|
||||
|
||||
Voila! Now if you check out what you've done, you'll find you're able to send data to your `/put` endpoint and respond with HTML updating the output `div`. Neato!
|
||||
|
||||
Let's retrieve the backend data we're now storing.
|
||||
|
||||
Add this to your server code:
|
||||
|
||||
```js
|
||||
app.get("/get", (req, res) => {
|
||||
setHeaders(res);
|
||||
|
||||
const output = `Backend State: ${JSON.stringify(backendData)}.`;
|
||||
let frag = `<div id="output2">${output}</div>`;
|
||||
|
||||
sendSSE({
|
||||
res,
|
||||
frag,
|
||||
selector: null,
|
||||
merge: true,
|
||||
mergeType: "morph_element",
|
||||
end: true,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
And this to your HTML:
|
||||
|
||||
```html
|
||||
<button data-on-click="$$get('/get')">Get Backend State</button>
|
||||
<div id="output2"></div>
|
||||
```
|
||||
|
||||
We're now fetching state that's stored on the backend.
|
||||
|
||||
Let's try something for fun. In your `/get` route, change your call to `sendSSE` so that we do not immediately end the request connection.
|
||||
|
||||
Change your `sendSSE` function call in your `\get` route.
|
||||
|
||||
```js
|
||||
sendSSE({
|
||||
...
|
||||
end: false,
|
||||
});
|
||||
```
|
||||
|
||||
Add this to your `sendSSE` function below the first call:
|
||||
|
||||
```js
|
||||
frag = `<div id="output3">Check this out!</div>;`;
|
||||
sendSSE({
|
||||
res,
|
||||
frag,
|
||||
selector: "#main",
|
||||
merge: true,
|
||||
mergeType: "prepend_element",
|
||||
end: true,
|
||||
});
|
||||
```
|
||||
|
||||
Now you'll notice you're sending two events in one call. That's because Datastar uses SSE. So using `prepend_element` we're able to prepend what we want to a target element. We do this using a `selector` and in our case this is the `<main>` element. Good stuff! You can check out all of Datastar's event types [here](http://localhost:8080/reference/plugins_backend).
|
||||
|
||||
There's one last thing we're going to do. Let's add a simple data feed upon loading the page.
|
||||
|
||||
Copy this to your server code:
|
||||
|
||||
```js
|
||||
app.get("/feed", async (req, res) => {
|
||||
setHeaders(res);
|
||||
while (res.writable) {
|
||||
const rand = randomBytes(8).toString("hex");
|
||||
const frag = `<span id="feed">${rand}</span>`;
|
||||
sendSSE({
|
||||
res,
|
||||
frag,
|
||||
selector: null,
|
||||
merge: false,
|
||||
mergeType: null,
|
||||
end: false,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
|
||||
Add this inside your `<main>` element:
|
||||
|
||||
```html
|
||||
<div>
|
||||
<span>Feed from server: </span>
|
||||
<span id="feed" data-on-load="$$get('/feed')"></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
I told you we would use another `data-on` action earlier and here it is. `data-on-load` will perform this request when the page loads. If you check things out now you should see a feed that updates using SSE upon loading. Cool!
|
||||
|
||||
Datastar supports all the verbs without requiring a `<form>` element: `GET, POST, PUT, PATCH, DELETE`.
|
||||
|
||||
So that concludes our primer! Check out the full code for our Node example [here](/examples/node).
|
||||
|
||||
If you're still here I imagine you want to know more. Let's define things a little better.
|
||||
|
||||
## A Better View
|
||||
|
||||
To be more precise, think of Datastar as an extension to HTML's [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). Using attributes, you can introduce state to your frontend, then access it anywhere in your DOM, or a backend of your choice. You can also setup events that trigger endpoints, then respond with HTML that targets fragments of your DOM.
|
||||
|
||||
- Declare global state: `data-store = "{foo: ''}"`
|
||||
- Link-up HTML elements to state slots: `data-model = "foo"`
|
||||
- Adjust HTML elements text content: `data-text = "$foo"`
|
||||
- Hookup other effects on your DOM to the state: `data-show= "$foo"`
|
||||
- Setup events using `data-on-(load or click) = "$$get(/endpoint)"`
|
||||
- Respond in HTML wrapped in SSE with a target element ID to update
|
||||
|
||||
It's that simple. To dive deeper check out some of the other links or just click below.
|
|
@ -1,15 +0,0 @@
|
|||
# Hypermedia, lost knowledge and H.A.T.E.O.A.S.
|
||||
|
||||
The rise of the frontend framework wars makes sense. When compared with full page reloads, the ability to update only the parts of the page that need to change is a huge win. However, the way this is done is often at odds with the declarative nature of HTML. This has led to a lot of lost knowledge and a lot of confusion. When your application logic lives in both browser and backend your synchronization state is in constant flux.
|
||||
|
||||
One of the biggest loses of knowledge is the concept of hypermedia. Hypermedia is the idea that the web is a set resources that are interconnected. This is the basis of the web and the reason it has been so successful. However, the rise of the frontend framework wars has led to a lot of confusion about how to use hypermedia. This is where H.A.T.E.O.A.S. comes in.
|
||||
|
||||
## Hypermedia as the Engine of Application State (H.A.T.E.O.A.S.)
|
||||
Browsers don't care about your application, they care about the rendering hypermedia. For example if you have a bank website you can go to the home page and then click on a link to go to the login page, only then are valid links to your accounts available. This has huge benefits.
|
||||
1. Each interaction fuels the valid next state.
|
||||
2. When implemented correctly this means the backend is where all your logic lives, leading to no client side routers, validation, etc.
|
||||
3. HTML can be generated in any language.
|
||||
|
||||
## Hypermedia on whatever you Like (HOWL)
|
||||
|
||||
[Carson Gross](https://hypermedia.systems/) coined the term [HOWL stack](https://htmx.org/essays/hypermedia-on-whatever-youd-like/) which is really just getting back to the roots of the web. People make fun of HTML not being a real language but its a wonderful way of exchanging interconnect assets. The HOWL stack is about leveraging the web's capabilities and with just a thin set of shims make it as powerful as any SPA framework. This is the basis of Datastar.
|
|
@ -1,65 +0,0 @@
|
|||
# Streaming Backend
|
||||
|
||||
HTMX is amazing for resurrecting the server rendered web. In general its far easier and faster than SPA development but there are still a few issues.
|
||||
|
||||
- For any larger page you have to add [hyperscript](https://hyperscript.org/) or [Alpine.js](https://alpinejs.dev/) to make the page reactive.
|
||||
- There are multiple ways to target the elements on the page with `hx-target` and [out of band swaps](https://htmx.org/attributes/hx-swap-oob/). This if flexible but leads to a ton of ways to do the same thing.
|
||||
- Responses are still single responses. If you want to have multiple updates as soon as possible you need to use plugins that aren't well supported (at this time).
|
||||
- The list of `hx-*` attributes is growing quickly and think it points to a bigger issue with scope.
|
||||
|
||||
None of these are deal breakers but starting from first principles we can do better. The first thing we need to do is make the page reactive. We can do this by using [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) or [Websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). We'll use version of SSE because
|
||||
|
||||
- At the end of the day its "just" a HTTP request. All your sessions, cookies, etc are still valid.
|
||||
- Websockets are harder to deal with depending on sticky sessions and load balancers.
|
||||
- Websockets aren't compatible with HTTP2/3. Datastar will probably move to [WebTransport](https://web.dev/webtransport/) when its more available (looking at you Apple).
|
||||
- HTTP2/3 is already multiplexed so you can have multiple SSE streams open at once.
|
||||
|
||||
## SSE
|
||||
|
||||
If you aren't familiar with SSE its a way to a chunked response to the browser. The browser will automatically reconnect if the connection is lost. The server can also send a `retry` header to tell the browser how often to reconnect. The browser will also automatically reconnect if the connection is lost. The nice part of having a chunked response is you can send multiple updates or a single update and the browser will handle it. A modern backend HTTP server framework should allow for easy SSE support (it's just a few headers and a specific text format for content).
|
||||
|
||||
## SSE backend, fetch on the frontend
|
||||
|
||||
For a reason that I still haven't figured out, the SSE spec and [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) only supports GET requests. This seems like a huge oversight, but we can work around it. From the backend's perspective they can send a `Content-Type: text/event-stream` from any request. This means we can use a normal `fetch` request to get the SSE stream. The only downside is we have to manually handle the reconnects. This is handled by Datastar but it's good to be aware of.
|
||||
|
||||
## Let's get some data
|
||||
|
||||
On the [home page](/) there is a global store example. When you first load the page the contents is basically empty.
|
||||
|
||||
```html
|
||||
<div id="global-count-example" data-on-load="$$get('/api/globalCount')">
|
||||
<span>Loading example on delay...</span>
|
||||
<div class="spinner datastar-indicator"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Normally you don't need to do this but its trying to show that you can load a page and then have the data update. The `$$get` action is a helper function that will make a fetch request and update the store with the results. The `datastar-indicator` class is a helper class that will show a spinner when the request is in flight.
|
||||
|
||||
On the backend (Go in this case) we return a set of render fragments in the form of a set of `text/event-stream` events. In this case it's just a single event.
|
||||
|
||||
```go
|
||||
event: datastar-fragment
|
||||
id: 129618219840307262
|
||||
data: selector
|
||||
data: merge morph_element
|
||||
data: settle 0
|
||||
data: fragment <div ...>...</div>
|
||||
|
||||
|
||||
```
|
||||
|
||||
The `selector` is the CSS selector that the fragment will be inserted into. The `merge` decides how the element is added to the page. `morph_element` uses [idiomorph](https://github.com/bigskysoftware/idiomorph) to merge into the page intelligently. The `settle` is the time in milliseconds that the fragment will be inserted. The `fragment` is the HTML that will be inserted into the page. If you are familiar with HTMX and the the structure of SSE messages this should look quite familiar. Nice thing is most of these are optional (except for `fragment`) and can be optionally added to the response. In the case of the Go implementation it includes all for the sake of completeness.
|
||||
|
||||
This is still a lot to explore but the main point is that we can have a reactive page without having to write any Javascript. This is the power of declarative code. I'd suggest you check out the [examples](/examples) to see how this all fits together. Then when you have a specific question check out the [reference](/reference) for more details.
|
||||
|
||||
# Conclusion
|
||||
|
||||
Please don't hesitate to reach out if you have any questions. We are always looking for feedback and ways to improve. If you are interested in contributing please check out the [Github](https://github.com/delaneyj/datastar)
|
||||
|
||||
# Choose your own adventure
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
<a href="/examples" class="flex-1 no-underline p-4 rounded-md bg-primary-600 text-center hover:bg-primary-500">Show me</a>
|
||||
<a href="/reference" class="flex-1 no-underline p-4 rounded-md bg-primary-600 text-center hover:bg-primary-500">Let me dig in</a>
|
||||
<a href="/essays" class="flex-1 no-underline p-4 rounded-md bg-primary-600 text-center hover:bg-primary-500">Pontificating</a>
|
||||
</div>
|
|
@ -1,134 +0,0 @@
|
|||
# Von Deepa
|
||||
|
||||
## Javascript Fatigue
|
||||
|
||||
Web development has become a technical occultism activity, in which the focus is on JavaScript and the capabilities of making HTML content dynamic instead of making a better job of delivering HTML. This JavaScript religion has led to the rise of frontend frameworks such as React, Vue, Svelte, Solid, etc. In turn those were not enough for full application and progress led the industry to full-stack JavaScript frameworks like Next.js, Nuxt, Svelte and Solid Start. Once you need a framework for reactivity it makes sense to embrace it in the backend too for consistency.
|
||||
|
||||
In reality almost all frameworks come down to updating the DOM as fast and as simply as possible with some effort around improving developer experience.
|
||||
|
||||
## Philosophy
|
||||
|
||||
- **Be declarative**
|
||||
- **Use signals**
|
||||
- **Supply a set of plugins that handle 99% of problems**
|
||||
|
||||
Datastar started as just a plugin framework but found that by having no overlap in features, it was possible to replace any SPA framework and even hypermedia focused libraries like HTMX while being much smaller and _(we think)_ easier to use.
|
||||
|
||||
With Datastar, even if you have never coded before, with a few examples, you can easily create high interconnected web assets. It doesn't matter if you are a making a user interface for bank or a simple blog. The approach is simplicity through declarative HTML.
|
||||
|
||||
If Datastar doesn't match your needs, you still might be interested in using it as originally intended [and write your own library](https://github.com/delaneyj/datastar/tree/main/library/src/lib/plugins).
|
||||
|
||||
## Time to think declaratively
|
||||
|
||||
Declarative code is amazing.
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE state='completed'
|
||||
ORDER BY time DESC
|
||||
LIMIT 10
|
||||
```
|
||||
|
||||
This is SQL. As a user you don't have to know how the query will get executed, its up to the engine used. Sure you can read the query plan or introspect indexes but you can start at a high level and drill down only when necessary.
|
||||
|
||||
HTML work in a similar fashion. You don't have to worry about how a `<div>Hello</div>` turned into pixels nor how the page uses resources when you tab away. This is wonderful for the majority of cases but at least in modern HTML is a bit limiting.
|
||||
|
||||
For example:
|
||||
|
||||
```html
|
||||
...PSEUDO CODE..
|
||||
<div>
|
||||
<label>HELLO WORLD</label>
|
||||
<input value="hello world" />
|
||||
</div>
|
||||
```
|
||||
|
||||
if you want to use the result of changing the input to modify the label you have to write code. What if we could extend HTML to make that process declarative? Well funny you ask...
|
||||
|
||||
## Custom data attributes
|
||||
|
||||
Luckily HTML5 has the concept of [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) which allows anyone to add thier own attributes and still be valid HTML. The only real requirement is they be kebab-cased and start with `data-*`. Data star... that'd be a clever name... oh I get it!
|
||||
|
||||
Before we can actually use the `data-*` we need a quick aside about signals.
|
||||
|
||||
## Signals
|
||||
|
||||
Signals are a way to do fine grain reactivity in a very efficient way. They are similar to formulas in Excel. Instead of doing something like:
|
||||
|
||||
```js
|
||||
let a = 2;
|
||||
let b = 2;
|
||||
let c = a * b;
|
||||
console.log(c);
|
||||
```
|
||||
|
||||
You can do:
|
||||
|
||||
```js
|
||||
const a = signal(2);
|
||||
const b = signal(2);
|
||||
const c = computed(() => a() * b());
|
||||
effect(() => {
|
||||
console.log(c());
|
||||
});
|
||||
```
|
||||
|
||||
The difference is if you change `a` or `b`, `c` will auto schedule for updates. So you spend your time declaring relationships more than procedures. They have been popularized by [Solid,js](https://www.solidjs.com/) but are now used by many frameworks.
|
||||
|
||||
## The Store
|
||||
|
||||
Ok so back to our hypothetical framework let's have a way to declare stuff that can setup signals on the page using `data-*` attributes.
|
||||
|
||||
```html
|
||||
<div data-store="{value:'hello world'}"></div>
|
||||
```
|
||||
|
||||
The contents is just a set of data that can evaluated by [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) and will convert this data into a tree of signals and merge into a store that tracks all of the reactivity on the page. In this case we want there to be a a single `value` signal with the contents of `'hello world'`.
|
||||
|
||||
## Models
|
||||
|
||||
Let's replace the hard coded value in the input with another attribute
|
||||
|
||||
```html
|
||||
<div data-store="{value:'hello world'}">
|
||||
<label>HELLO WORLD</label>
|
||||
<input data-model="value" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Here we've created a new attribute `data-model` with the contents of `value`. We are just saying when the signal `value` changes **or** input is edited on the page make sure you keep them in sync. We don't care how, just do it.
|
||||
|
||||
## Contents
|
||||
|
||||
Now we want to update the label relationship.
|
||||
|
||||
```html
|
||||
<div data-store="{value:'hello world'}">
|
||||
<label data-text="$value"></label>
|
||||
<input data-model="value" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Here we've added another attribute `data-text` but the content has a `$` prefix. Remember that `data-*` attributes _are just strings_, which means we can give any semantics we want as long as it's consistent. It this case we are designating the use of a signal by adding a `$` prefix. Now went the `value` signal updates, so will the `innerHTML` of the label. Pretty neat.
|
||||
|
||||
However it doesn't yet match the original intent, which was to make it uppercase, so let's make a quick adjustment.
|
||||
|
||||
```html
|
||||
<div data-store="{value:'hello world'}">
|
||||
<label data-text="$value.toUpperCase()"></label>
|
||||
<input data-model="value" />
|
||||
</div>
|
||||
```
|
||||
|
||||
So with this change and in a declarative nature you'd be able extend HTML and focus on relationships.
|
||||
|
||||
Our HTML looks pretty neat but what would it take to make it actually work? Well just add:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script>
|
||||
```
|
||||
|
||||
somewhere on your page and it should **_just work_**. If you look at the dev tools of your browser you'll also see its tiny compared to any other full framework and we haven't scratched the surface of what's included.
|
||||
|
||||
So far looking a lot like [Alpine.js](https://alpinejs.dev/) which is great, but as we add more features will diverge.
|
|
@ -1,23 +0,0 @@
|
|||
_September 1 2023_
|
||||
|
||||
# Why another framework?
|
||||
|
||||
## What's different?
|
||||
|
||||
There are many things I find wonderful about Alpine.js and HTMX. However there are a few things that I think could be improved. The biggest challenge it both is plugins (called `directives`/`magic` in Alpine and `extensions` in HTMX). I tried writing in both of them as was disappointed by the breadth of monkey patching and lack of type safety. In general you don't have access to what other plugins do nor can you build a DAG of dependencies. This means you have to be very careful about the order you load plugins and what they do. This is especially true if you want to use a plugin in a plugin. I think this is a big problem and I wanted to solve it. Because Datastar started with the notion **_EVERYTHING_** is a plugin you could theoretically build both Alpine and HTMX inside of Datastar. I don't think that is a good idea but it is possible.
|
||||
|
||||
Beyond that I want the backend to be the _source of truth_ when it comes to state. [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) is a great way to structure your applications but you still need some client side state. Datstar does not use FormData; it lacks the ability to define nested data structures simply. So in general you send up JSON (automatically) and down HTML. This is more prescriptive than HTMX, but again this is with just the included plugins. You can build your own plugins to do whatever you want.
|
||||
|
||||
## Background
|
||||
|
||||
I love what [Caleb Porzio](https://calebporzio.com/) has done with [Alpine.js](https://alpinejs.dev/) and likewise with [Carson Gross](https://bigsky.software/cv/) with [HTMX](https://htmx.org/). However after trying to make extensions in both I got disappointed with the limitations. Both are pure Javascript which is fine but leads to having to either keep the whole state in your head and or be prepared for lots of runtime errors. Personally I think the best way to do this is to use Typescript and have the build step and compiler catch as many errors as possible. Plus it makes it easier to extend and create optimized builds.
|
||||
|
||||
### Why not just use Vue/React/Svelte?
|
||||
|
||||
[HTMX Essays](https://htmx.org/essays/) are a great place to start.
|
||||
|
||||
### Why not just use HTMX+Alpine?
|
||||
|
||||
There was a bit of a rant here before. TL;DR tried to show HTMX v2 could do all these things and it wasn't accepted by the community. I think HTMX is great and I will continue to recommend it. In general I think the some of the choices are throwing the baby out with the bathwater when it comes to things like Vite/TS/etc. Good news is that everything I, and some other lurkers, wanted are available in Datastar or can be added easily via plugins.
|
||||
|
||||
[Let's move on to the docs and see what Datastar can do](/docs)
|
|
@ -1,84 +0,0 @@
|
|||
_September 7, 2023_
|
||||
|
||||
# Yes, you want a build step
|
||||
|
||||
## In response to the HTMX essay [No Build Step](https://htmx.org/essays/no-build-step/)
|
||||
|
||||
This is making the counter argument that a build step are extremely valuable when building a frontend framework. Lack of builds might be the right choice for HTMX but in my eyes its a self full-filling prophecy. The author concurrently wary of progress yet works on a library about extending the HTML spec to do more than it was designed for. The essay is written by a core contributor and not the author of HTMX. In general, its filled with so many fallacies but the real culprit is the _false dilemma fallacy_.
|
||||
|
||||
> This common fallacy misleads by presenting complex issues in terms of two inherently opposed sides. Instead of acknowledging that most (if not all) issues can be thought of on a spectrum of possibilities and stances, the false dilemma fallacy asserts that there are only two mutually exclusive outcomes.
|
||||
> This fallacy is particularly problematic because it can lend false credence to extreme stances, ignoring opportunities for compromise or chances to re-frame the issue in a new way.
|
||||
|
||||
The whole discussion is framed as a false dilemma. Almost every point he makes is demonstrably false by the counter example of Datastar. Its smaller, faster, supports more features, isn't written in Javascript, targets multiple browser versions, and has a build step. If he is trying to prove his point it needs to be done with more rigor. In general I find the HTMX essays to be a beacon for well articulated arguments; this one is not and does a disservice to the community exploring hypermedia.
|
||||
|
||||
## TED Talk
|
||||
|
||||
In the first paragraph he says both:
|
||||
|
||||
> A recurring question from some htmx contributors is why htmx isn’t written in TypeScript, or, for that matter, why htmx **_lacks any build step at all_**. The full htmx source is a single 3,500-line JavaScript file; if you want to contribute to htmx, you do so by modifying the htmx.js file, the same file that gets sent to browsers in production, **_give or take minification and compression_**.
|
||||
|
||||
So before the essay starts he's given up the ghost. Even in as close to pure Javascript as you can get you still need a build step. Thank you, this has been my TED talk.
|
||||
|
||||
## Write once, run forever
|
||||
|
||||
> The best reason to write a library in plain JavaScript is that it lasts forever.
|
||||
|
||||
This is just not the world we live in. ES5 is dead, IE11 is dead, ECMAScript doesn't look anything like it did a decade ago, let alone 20 years ago. ECMAScript has been around for 25 years and has changed a lot in that time, let alone the APIs you are interfacing with. The web is a moving target and you need to keep up with it. If you don't you will be left behind.
|
||||
|
||||
## Reduction in labor
|
||||
|
||||
> Maintenance is a cost paid for with labor, and open-source code bases are the projects that can least afford to pay it. Opting not to use a build step drastically minimizes the labor required to keep htmx up-to-date.
|
||||
|
||||
No, just no. Much like CI/CD pipelines you set it up and largely forget about it. You can even automate the updates. The build step is a one time cost that pays dividends for the life of the project. If you don't have a build step you are going to be spending a lot of time manually updating your codebase. This is a false dichotomy. Datastar supports IIFE, UMD, & Modules and took all of adding the options to the build step. I've seen the crazy wrapper that HTMX uses to support all the different module types. It's a lot of code and a lot of maintenance. I'm not saying that HTMX is doing it wrong, just that it's a lot of work to support all the different module types. I'm not sure how much time it saves in the long run.
|
||||
|
||||
> But htmx is written in JavaScript, with no dependencies, so it will run unmodified for as long as web browsers remain relevant. Let the browser vendors do the hard work for you.
|
||||
|
||||
Every Datastar build or CDN version is a single Javascript file with no dependencies. The FUD here doesn't match reality at sets up a narrative of purity that is just not true. If fact oddly **because** of our build step we have full source mapping supported so you can debug your the production minified version in the browser looking at the original source and stack. This is a huge win for developers. I'm not sure why you would want to give that up.
|
||||
|
||||
## Source Maps and Hot Module Reloading
|
||||
|
||||
> Build step advocates point out that TypeScript can generate source maps, which tell your browser what TypeScript corresponds to what JavaScript, and that’s true! But now you have another thing to keep track of—the TypeScript you wrote, the JavaScript it generated, and the source map that connects these two. The hot-reloading development server you’re now dependent on will keep these up to date for you on localhost
|
||||
|
||||
Yes you have **_the same build folder_** with more static files that reference each other. This is not a big deal, a weird straw man, non-issue. You have to run a dev server to test your code anyway for things like CORS to work. This is just a weird argument, especially in light of things like hot module reloading for both code and styles. Modern tooling is now in the _sub milliseconds_ for rebuilds. Want to run your tests in parallel, well now you are just missing out on the real DX that modern tooling provides.
|
||||
|
||||
> ...but what about on your staging server? What about in production? Bugs that appear in these environments will be harder to track down, because you’ve lost a lot of information about where they come from.
|
||||
|
||||
Github actions for `bun build` or `vite build` are dead easy, and with weird arguments that are just not an issue nowadays. Like complaining about Makefiles when you have Visual Studio.
|
||||
|
||||
## The DX
|
||||
|
||||
> The htmx DX is very simple—your browser loads a single file, which in every environment is the exact same file you wrote. The tradeoffs required to maintain that experience are real, but they’re tradeoffs that make sense for this project.
|
||||
|
||||
The DX is why I personally moved on. The core monkey patches the DOM and that's just not a good DX. The core is not very extendable and you have to keep all 3000 LOC in your head to understand the ramification of your changes let alone the changes that other people make. You are giving up a lot of the DX that you get with a build step.
|
||||
|
||||
> Requiring that htmx remain in a single file (again, around 3,500 LOC) enforces a degree of intention on the library; there is a real pressure when working on the htmx source to justify the addition of new code, a pressure which maintains an equilibrium of relative simplicity.
|
||||
|
||||
Or... make everything an extension and allow them to rely on each other at there discretion. Making it a single file doesn't do anything but make harder to maintain.
|
||||
|
||||
## Amalgamation is a build step
|
||||
|
||||
> the lack of places for functionality to hide makes working on htmx a lot more approachable. Far, far more complex projects use aspects of this approach as well: SQLite3 compiles from a single-file source amalgamation (though they use separate files for development, they’re not crazy) which makes hacking on it significantly easier. You could never build the linux kernel this way—but htmx is not the linux kernel.
|
||||
|
||||
This is one of the worst takes in the whole essay. SQLite is **developed** as a set of files and **deployed** as an amalgamation. Just like Datastar, Alpine, HTMX, Zod, etc. You are conflating distribution with development and even making examples against you're own position.
|
||||
|
||||
> The htmx code does have to be written in JavaScript, though, because browsers run JavaScript. And as long as JavaScript is dynamically typed, the tradeoffs required to get true static typing in the htmx source are not worth it (htmx users can still take advantage of typed APIs, declared with .d.ts files).
|
||||
|
||||
The declaration (.d.ts) he point do are not accurate and aren't maintained last I checked. Also they are filled with `any` escape hatches which don't say anything. Separating definition into documentation seems to always cause this to happen. Conflating a build target with a language target. Like saying you have write in Assembly because x86 run assembly. Javascript can be a runtime target just like C can compile to assembly. You can write in Typescript and compile to Javascript. Another fallacy.
|
||||
|
||||
## Legacy targets stop progress
|
||||
|
||||
> Because htmx maintains support for Internet Explorer 11, and because it does not have a build step, every line of htmx has to be written in IE11-compatible JavaScript, which means no ES6.
|
||||
|
||||
This is certainly a choice and have seems as a hot button debate in the HTMX discord channel. If you had a build step, let alone used Typescript, you could target both **with the same codebase**. The false dilemma fallacy.
|
||||
|
||||
> This point is obvious, but it’s worth re-stating: the htmx source would be a lot tidier if it could be split it into modules. There are other factors that affect code quality besides tidiness, but to the extent that the htmx source is high-quality, it is not because it is tidy.
|
||||
|
||||
This is just a weird argument. You can split it into modules and still have tidiness. You can still split up into extensions, I know because I did it. To be clear, this isn't a declaration of minimal code sizes, but it _should_ be broken up into at least a per module/extension basis.
|
||||
|
||||
> Once you no longer have an enormous codebase of frontend JavaScript, there is far less pressure to adopt JavaScript on the backend. You can write backends in Python, Go, even NodeJS, and it doesn’t matter to htmx—every mainstream language has mature solutions for formatting HTML. This is the principle of Hypermedia On Whatever you’d Like (HOWL).
|
||||
|
||||
This essay is about HTMX build step, not the backend. It appears to me that not only is it clear you are throwing out the baby with the bathwater but demolishing the whole house. Just because SPA's are not the right tool for every job doesn't mean that you should throw out the whole tool box. We are talking about the best way to build a frontend library, not the backend.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Even HTMX has a build step, and if they embraced it they could support multiple targets while still keeping their single un-tree shakeable amalgamation that they enjoy developing with.
|
|
@ -1,28 +0,0 @@
|
|||
## Active Search
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/active-search/)
|
||||
|
||||
## Explanation
|
||||
|
||||
This example actively searches a contacts database as the user enters text.
|
||||
|
||||
## Demo
|
||||
|
||||
<div>
|
||||
<div id="active_search" data-on-load="$$get('/examples/active_search/data')">
|
||||
Replace me
|
||||
</div>
|
||||
</div>
|
||||
|
||||
The interesting part is the input field:
|
||||
|
||||
```html
|
||||
<input
|
||||
data-model="search"
|
||||
data-on-input.debounce_1000ms="$$get('/examples/active_search/data')"
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
/>
|
||||
```
|
||||
|
||||
The input issues a `GET` to `/active_search/data` with the input value bound to `$search`. The `debounce_1000ms` modifier ensures that the search is not issued on every keystroke, but only after the user has stopped typing for 1 second. This modifiers will be covered in more detail in the [reference section](/reference).
|
|
@ -1,75 +0,0 @@
|
|||
# Animations
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/animations/)
|
||||
|
||||
# Explanation
|
||||
|
||||
Datastar is designed to allow you to use CSS transitions to add smooth animations and transitions to your web page using only CSS and HTML. Below are a few examples of various animation techniques.
|
||||
|
||||
Datastar also allows you to use the new View Transitions API for creating animations.
|
||||
|
||||
<div id="animations" data-on-load="$$get('/examples/animations/data')">
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Basic CSS Animations
|
||||
|
||||
### Color Throb
|
||||
|
||||
<div>
|
||||
<div id="color_throb">Color Throb Demo</div>
|
||||
</div>
|
||||
|
||||
The simplest animation technique in Datastar is to keep the id of an element stable across a content swap. If the id of an element is kept stable, htmx will swap it in such a way that CSS transitions can be written between the old version of the element and the new one.
|
||||
|
||||
Consider this div:
|
||||
|
||||
```html
|
||||
<div
|
||||
id="color_throb"
|
||||
class="transition-all duration-1000 font-bold text-2xl text-center rounded-box p-4 uppercase"
|
||||
style="background-color:#b57614;color:#83a598"
|
||||
>
|
||||
blue on yellow
|
||||
</div>
|
||||
```
|
||||
|
||||
With SSE we just update the style every second:
|
||||
|
||||
### Smooth Progress Bar
|
||||
|
||||
The [Progress Bar](/examples/progress_bar) demo uses this basic CSS animation technique as well, by updating the `length` property of a progress bar element, allowing for a smooth animation.
|
||||
|
||||
## Swap Transitions
|
||||
|
||||
### Fade Out On Swap
|
||||
|
||||
<div id="fade_out_swap">Fade out swap Demo</div>
|
||||
|
||||
If you want to fade out an element that is going to be removed when the request ends, just send a SSE event with the opacity set to 0 and set a transition duration. This will fade out the element before it is removed.
|
||||
|
||||
## Settling Transitions
|
||||
|
||||
### Fade In On Addition
|
||||
|
||||
<div id="fade_me_in">Fade in Demo</div>
|
||||
|
||||
Building on the last example, we can fade in the new content the same way, starting from an opacity of 0 and transitioning to an opacity of 1.
|
||||
|
||||
## Request In Flight Animation
|
||||
|
||||
<div id="request_in_flight">Request in Flight Demo</div>
|
||||
|
||||
One of the nice features for reactivity is to show a spinner when a request is in flight. On any element that is using backend actions you can add a `data-indicator` attribute to show a spinner when the request is in flight. This can be done like so:
|
||||
|
||||
```html
|
||||
<button
|
||||
data-fetch-indicator="'#request_in_flight_indicator'"
|
||||
data-on-click="$$post('/examples/animations/data/request_in_flight')"
|
||||
id="submit_request_in_flight"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
```
|
||||
|
||||
This will show the element with the id `request_in_flight_indicator` when the request is in flight and hide it when the request is complete.
|
|
@ -1,57 +0,0 @@
|
|||
## Bulk Update
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/bulk-update/)
|
||||
|
||||
## Demo
|
||||
|
||||
<style>
|
||||
tr.deactivate.datastar-settling td {
|
||||
background: lightcoral;
|
||||
}
|
||||
tr.activate.datastar-settling td {
|
||||
background: darkseagreen;
|
||||
}
|
||||
tr td {
|
||||
transition: all 1.2s;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="bulk_update"
|
||||
data-on-load="$$get('/examples/bulk_update/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is accomplished by putting a form around a table, with checkboxes in the table, and then including the checked values in PUT’s to two different endpoints: `activate` and `deactivate`:
|
||||
|
||||
Added to the page in this way:
|
||||
|
||||
```html
|
||||
<style>
|
||||
tr.deactivate.datastar-settling td {
|
||||
background: lightcoral;
|
||||
}
|
||||
tr.activate.datastar-settling td {
|
||||
background: darkseagreen;
|
||||
}
|
||||
tr td {
|
||||
transition: all 1.2s;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
The server will either activate or deactivate the checked users and then rerender the tbody tag with updated rows. It will apply the class activate or deactivate to rows that have been mutated. This allows us to use a bit of CSS to flash a color helping the user see what happened. The server events look like this:
|
||||
|
||||
```go
|
||||
event: datastar-fragment
|
||||
id: 129798448043016254
|
||||
data: selector
|
||||
data: merge upsert_attributes
|
||||
data: settle 500
|
||||
data: fragment ...
|
||||
```
|
||||
|
||||
Notice the `settle` value is 500. This means the `datastar-settling` class will be added immediately and removed after 500ms. This is a nice way to show the user that something is happening.
|
|
@ -1,77 +0,0 @@
|
|||
## Click to Edit
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/click-to-edit/)
|
||||
|
||||
## Demo
|
||||
|
||||
<div
|
||||
id="contact_1"
|
||||
data-on-load="$$get('/examples/click_to_edit/contact/1')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
The click to edit pattern provides a way to offer inline editing of all or part of a record without a page refresh. This pattern starts with a UI that shows the details of a contact. The div has a button that will get the editing UI for the contact from `/contact/1/edit`
|
||||
|
||||
```html
|
||||
<!-- Removed styling -->
|
||||
<div id="contact_1">
|
||||
<label>First Name: John</label>
|
||||
<label>Last Name: Doe</label>
|
||||
<label>Email: joe@blow.com</label>
|
||||
<div>
|
||||
<button data-on-click="$$get('/examples/click_to_edit/contact/1/edit')">
|
||||
Edit
|
||||
</button>
|
||||
<button data-on-click="$$get('/examples/click_to_edit/contact/1/reset')">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
This returns a form that can be used to edit the contact
|
||||
|
||||
```html
|
||||
<!-- Removed styling and escaping for brevity -->
|
||||
<div
|
||||
id="contact_1"
|
||||
data-store="{
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'joe@blow.com'
|
||||
}"
|
||||
>
|
||||
<div class="form-control">
|
||||
<label>First Name</label>
|
||||
<input type="text" data-model="firstName" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Last Name</label>
|
||||
<input type="text" data-model="lastName" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Email</label>
|
||||
<input type="text" data-model="email" />
|
||||
</div>
|
||||
<div>
|
||||
<button data-on-click="$$put('/examples/click_to_edit/contact/1')">
|
||||
Save
|
||||
</button>
|
||||
<button data-on-click="$$get('/examples/click_to_edit/contact/1')">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### There is no form
|
||||
|
||||
If you compare to HTMX you'll notice there is no form, you can use one but its unnecessary. This is because you are already using signals and when you use a `PUT` to `/contact/1/edit`, the body is the entire contents of the store and its available to handle errors and validation holistically. There is also a profanity filter on the normal rendering of the contact that is not applied to the edit form. Controlling the rendering complete on the server allows you to have a single source of truth for the data and the rendering.
|
||||
|
||||
### There is no client validation
|
||||
|
||||
On the backend we've also added a quick santizer on the input to avoid bad actors (to some degree). You already have to deal with the data on the server so you might as well do the validation there. In this case its just modifying how the text is rendered when not editing. This is a simple example but you can see how you can extend it to more complex forms.
|
|
@ -1,29 +0,0 @@
|
|||
## Dialogs
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/dialogs/)
|
||||
|
||||
## Demo
|
||||
|
||||
<div
|
||||
id="dialogs"
|
||||
data-on-load="$$get('/examples/dialogs_browser/data')"
|
||||
>
|
||||
Replace me!
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
Dialogs can be triggered with the standard browser `prompt` and `confirm` within an expression. These are triggered by the user interaction that would trigger the fetch, but the request is only sent if the dialog is accepted.
|
||||
|
||||
```html
|
||||
<button
|
||||
id="dialogs"
|
||||
data-store="{prompt:'foo',confirm:false}"
|
||||
data-fetch-url=""
|
||||
data-on-click="$prompt=prompt('Enter a string',$prompt);$confirm=confirm('Are you sure?');$confirm && $$get('/examples/dialogs___browser/sure')"
|
||||
>
|
||||
Click Me
|
||||
</button>
|
||||
```
|
||||
|
||||
The value provided by the user to the prompt dialog is fed back into the store, and the confirm dialog is used to determine whether the request should be sent.
|
|
@ -1,47 +0,0 @@
|
|||
## Edit Row
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/edit-row/)
|
||||
|
||||
## Demo
|
||||
|
||||
<div
|
||||
id="edit_row"
|
||||
data-on-load="$$get('/examples/edit_row/data')"
|
||||
>
|
||||
Replace me!
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
This example shows how to implement editable rows. First let’s look at the row prior to editing:
|
||||
|
||||
```html
|
||||
<tr id="contact_0">
|
||||
<td>Joe Smith<//td>
|
||||
<td>joe@smith.org</td>
|
||||
<td>
|
||||
<button data-on-click="$editRowIndex=0;$$get('/examples/edit_row/edit')" >Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
This will tell trigger a whole table replacement as we are going to remove the `Edit` buttons from other rows as well as change out to inputs to allow editing. The `data-on-click` attribute sets a variable `$editRowIndex` to the index of the row that is being edited. This is used to determine which row to edit on the server side. Again we don't need alpine or hyperscript to do this, signals and actions are built-in.
|
||||
|
||||
Finally, here is what the row looks like when the data is being edited:
|
||||
|
||||
```html
|
||||
<tr id="contact_0">
|
||||
<td>
|
||||
<input data-model="name" type="text" />
|
||||
</td>
|
||||
<td>
|
||||
<input data-model="email" type="text" />
|
||||
</td>
|
||||
<td>
|
||||
<button data-on-click="$$get('/examples/edit_row/data')">Cancel</button>
|
||||
<button data-on-click="$$patch('/examples/edit_row/edit')">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
Here we have a few things going on, clicking `Cancel` will bring back the read-only version of the row. Finally, there is a save button that issues a `PATCH` to update the contact.
|
|
@ -1,33 +0,0 @@
|
|||
## Fetch Indicator
|
||||
|
||||
## Demo
|
||||
|
||||
<div id="ind">Loading Indicator</div>
|
||||
<button type="button" class="p-2 bg-accent-200 text-accent-700 shadow rounded" data-on-click="$$get('/examples/fetch_indicator/greet')" data-fetch-indicator="'#ind'" data-testid="greeting_button">Click me for a greeting</button>
|
||||
<div id="greeting"></div>
|
||||
|
||||
<div>Example from https://github.com/delaneyj/datastar/pull/24</div>
|
||||
<div data-store="{input: ''}">
|
||||
<input type="text" data-bind-readonly="$$isFetching('#submit')" data-model="input" class="bg-accent-900 border-2 border-accent-600 text-accent-100 text-sm rounded-lg focus:ring-primary-400 focus:border-primary-400 block w-full p-2.5" />
|
||||
<div data-show="$$isFetching('#submit')">Loading...</div>
|
||||
<button id="submit" data-bind-disabled="$$isFetching('#submit')" data-on-click="$$get('/examples/fetch_indicator/greet')">Submit</button>
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
```html
|
||||
<div id="ind">Loading Indicator</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 bg-accent-200 text-accent-700 shadow rounded"
|
||||
data-on-click="$$get('/examples/fetch_indicator/greet')"
|
||||
data-fetch-indicator="'#ind'"
|
||||
>
|
||||
Click me for a greeting
|
||||
</button>
|
||||
<div id="greeting"></div>
|
||||
```
|
||||
|
||||
The `data-fetch-indicator` attribute is used to specify the element that should be shown as a loading indicator while the fetch request is in progress. The value of the attribute is a CSS selector that selects the element to be shown as the loading indicator.
|
||||
|
||||
**Note:** The contents of the `data-fetch-indicator` is an expression. In this case, the expression is a string literal, hence the single quotes around the CSS selector.
|
|
@ -1,43 +0,0 @@
|
|||
## File Upload
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/file-upload/)
|
||||
|
||||
## Demo
|
||||
|
||||
<div
|
||||
id="file_upload"
|
||||
data-on-load="$$get('/examples/file_upload/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
In this example we show how to create a file upload form that will be submitted via fetch.
|
||||
|
||||
```html
|
||||
<!-- Removed styling for brevity -->
|
||||
<div
|
||||
id="file_upload" data-on-load="$$get"
|
||||
data-store="{"file":"","fileMime":"","fileName":""}"
|
||||
>
|
||||
<div>
|
||||
<label>
|
||||
<span class="label-text">Pick anything reasonably sized</span>
|
||||
</label>
|
||||
<input type="file" data-model="file">
|
||||
<button
|
||||
data-fetch-url="'/examples/file_upload/upload'"
|
||||
data-on-click="$$post">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
We don't need a form because everything is encoded as signals and automatically sent to the server.
|
||||
We `POST` the form to /upload, since the `input` is using `data-model` the file will be automatically encoded as base64. If your store includes `${signalName}Mime` and `${signalName}Name` then those will be sent as well.
|
||||
|
||||
## Differences from HTMX
|
||||
|
||||
Since HTMX uses standard forms the [file upload is lost](https://htmx.org/examples/file-upload-input/) when the form is submitted. This is a limitation of the browser and not HTMX. However, since we are using signals we can send the file back down to the client and hook up to the signal if desired.
|
|
@ -1,45 +0,0 @@
|
|||
## Infinite Scroll
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/infinite-scroll/)
|
||||
|
||||
## Explanation
|
||||
|
||||
The infinite scroll pattern provides a way to load content dynamically on user scrolling action.
|
||||
|
||||
Let’s focus on the final row (or the last element of your content):
|
||||
|
||||
```html
|
||||
<div
|
||||
data-intersects="$offset=0;$limit=10;$$get('/examples/infinite_scroll/data')"
|
||||
id="more_btn"
|
||||
>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
This last element contains a listener which, when scrolled into view, will trigger a request. The result is then appended after it. The last element of the results will itself contain the listener to load the next page of results, and so on. `data-intersects` is a custom attribute that triggers a request when the element is scrolled into view. The `$offset` and `$limit` parameters are used to control the pagination of the results. The server sents back a new `more_btn` element with a new offset and the results to be appended.
|
||||
|
||||
```bash
|
||||
event: datastar-fragment
|
||||
data: selector #more_btn
|
||||
data: merge morph_element
|
||||
data: settle 500
|
||||
data: fragment <div data-intersects="$offset=10;$limit=10;$$get('/examples/infinite_scroll/data')" id="more_btn"><div>Loading...</div></div>
|
||||
|
||||
event: datastar-fragment
|
||||
data: selector #click_to_load_rows
|
||||
data: merge append_element
|
||||
data: settle: 500
|
||||
data: fragment <<tr id=\"agent_60\"><td>Agent Smith 3c</td><td>void61@null.org</td><td class=\"uppercase\">39b02fcf39c047c5</td></tr>"
|
||||
|
||||
....More rows to be appended
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
<div
|
||||
id="infinite_scroll"
|
||||
data-on-load="$$get('/examples/infinite_scroll/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
|
@ -1,18 +0,0 @@
|
|||
## Inline Validation
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/inline-validation/)
|
||||
|
||||
## Demo
|
||||
|
||||
The only email that will be accepted is test@test.com.
|
||||
|
||||
<div
|
||||
id="inline_validation"
|
||||
data-on-load="$$get('/examples/inline_validation/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
This example shows how to do inline field validation, in this case of an email address. To do this we need to create a form with an input that POSTs back to the server with the value to be validated and updates the DOM with the validation results. Since its easy to replace the whole form the logic for displaying the validation results is kept simple.
|
|
@ -1,24 +0,0 @@
|
|||
## Is loading identifier
|
||||
|
||||
## Demo
|
||||
|
||||
<div data-show="$isLoading.has('get_greet')">Loading Signal</div>
|
||||
<button type="button" class="p-2 bg-accent-200 text-accent-700 shadow rounded" data-on-click="$$get('/examples/is_loading_identifier/greet')" data-is-loading-id="get_greet">Click me for a greeting</button>
|
||||
<div id="greeting"></div>
|
||||
|
||||
## Explanation
|
||||
|
||||
```html
|
||||
<div data-show="$isLoading.has('get_greet')">Loading Signal</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 bg-accent-200 text-accent-700 shadow rounded"
|
||||
data-on-click="$$get('/examples/is_loading_identifier/greet')"
|
||||
data-is-loading-id="get_greet"
|
||||
>
|
||||
Click me for a greeting
|
||||
</button>
|
||||
<div id="greeting"></div>
|
||||
```
|
||||
|
||||
The `data-is-loading-id` attribute is used to specify the name of the identifier that will be present in the global isLoading set when an element is fetching.
|
|
@ -1,33 +0,0 @@
|
|||
## Lazy Load
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/lazy-load/)
|
||||
|
||||
## Demo
|
||||
|
||||
<style>
|
||||
.datastar-settling {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="lazy_load" data-on-load="$$get('/examples/lazy_load/data')">
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
This example shows how to lazily load an element on a page. We start with an initial state that looks like this:
|
||||
|
||||
```html
|
||||
<div data-fetch-url="" data-on-load="$$get('/examples/lazy_load/graph')">
|
||||
Loading...
|
||||
</div>
|
||||
```
|
||||
|
||||
Which shows a progress indicator as we are loading the graph. The graph is then loaded and faded gently into view via a settling CSS transition:
|
||||
|
||||
```css
|
||||
.datastar-settling {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
|
@ -1,16 +0,0 @@
|
|||
## Lazy Tabs
|
||||
|
||||
[Original HTMX Version](https://htmx.org/examples/tabs-hateoas/)
|
||||
|
||||
## Demo
|
||||
|
||||
<div
|
||||
id="lazy_tabs"
|
||||
data-on-load="$$get('/examples/lazy_tabs/data')"
|
||||
>
|
||||
Replace me
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
This example shows how easy it is to implement tabs using Datastar. Following the principle of [Hypertext As The Engine Of Application State](https://en.wikipedia.org/wiki/HATEOAS), the selected tab is a part of the application state. Therefore, to display and select tabs in your application, simply include the tab markup in the returned HTML fragment.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue