+345
-18
package-lock.json
+345
-18
package-lock.json
···
9
9
"version": "0.0.1",
10
10
"dependencies": {
11
11
"@atproto/api": "^0.18.1",
12
+
"@lucide/svelte": "^0.555.0",
13
+
"qrcode": "^1.5.4",
12
14
"tldts": "^7.0.19"
13
15
},
14
16
"devDependencies": {
···
16
18
"@sveltejs/kit": "^2.49.0",
17
19
"@sveltejs/vite-plugin-svelte": "^6.2.1",
18
20
"@tailwindcss/vite": "^4.0.0",
21
+
"@types/qrcode": "^1.5.6",
19
22
"prettier": "^3.6.2",
20
23
"prettier-plugin-svelte": "^3.4.0",
21
24
"svelte": "^5.43.14",
···
550
553
"version": "0.3.13",
551
554
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
552
555
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
553
-
"dev": true,
554
556
"license": "MIT",
555
557
"dependencies": {
556
558
"@jridgewell/sourcemap-codec": "^1.5.0",
···
561
563
"version": "2.3.5",
562
564
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
563
565
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
564
-
"dev": true,
565
566
"license": "MIT",
566
567
"dependencies": {
567
568
"@jridgewell/gen-mapping": "^0.3.5",
···
572
573
"version": "3.1.2",
573
574
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
574
575
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
575
-
"dev": true,
576
576
"license": "MIT",
577
577
"engines": {
578
578
"node": ">=6.0.0"
···
582
582
"version": "1.5.5",
583
583
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
584
584
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
585
-
"dev": true,
586
585
"license": "MIT"
587
586
},
588
587
"node_modules/@jridgewell/trace-mapping": {
589
588
"version": "0.3.31",
590
589
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
591
590
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
592
-
"dev": true,
593
591
"license": "MIT",
594
592
"dependencies": {
595
593
"@jridgewell/resolve-uri": "^3.1.0",
596
594
"@jridgewell/sourcemap-codec": "^1.4.14"
597
595
}
598
596
},
597
+
"node_modules/@lucide/svelte": {
598
+
"version": "0.555.0",
599
+
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.555.0.tgz",
600
+
"integrity": "sha512-aqTOMjBjf/HNwrhggRdb83T0QslZdpJTyTwr/chtXTGw7u4Hcu4zQb/5uA+csF0KKawKWVnsNI1MdHEHeEXTcQ==",
601
+
"license": "ISC",
602
+
"peerDependencies": {
603
+
"svelte": "^5"
604
+
}
605
+
},
599
606
"node_modules/@polka/url": {
600
607
"version": "1.0.0-next.29",
601
608
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
···
922
929
"version": "1.0.7",
923
930
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz",
924
931
"integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==",
925
-
"dev": true,
926
932
"license": "MIT",
927
933
"peerDependencies": {
928
934
"acorn": "^8.9.0"
···
1301
1307
"version": "1.0.8",
1302
1308
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1303
1309
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1304
-
"dev": true,
1305
1310
"license": "MIT"
1306
1311
},
1312
+
"node_modules/@types/node": {
1313
+
"version": "24.10.1",
1314
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
1315
+
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
1316
+
"dev": true,
1317
+
"license": "MIT",
1318
+
"peer": true,
1319
+
"dependencies": {
1320
+
"undici-types": "~7.16.0"
1321
+
}
1322
+
},
1323
+
"node_modules/@types/qrcode": {
1324
+
"version": "1.5.6",
1325
+
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
1326
+
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
1327
+
"dev": true,
1328
+
"license": "MIT",
1329
+
"dependencies": {
1330
+
"@types/node": "*"
1331
+
}
1332
+
},
1307
1333
"node_modules/acorn": {
1308
1334
"version": "8.15.0",
1309
1335
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
1310
1336
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
1311
-
"dev": true,
1312
1337
"license": "MIT",
1313
1338
"peer": true,
1314
1339
"bin": {
···
1318
1343
"node": ">=0.4.0"
1319
1344
}
1320
1345
},
1346
+
"node_modules/ansi-regex": {
1347
+
"version": "5.0.1",
1348
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1349
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1350
+
"license": "MIT",
1351
+
"engines": {
1352
+
"node": ">=8"
1353
+
}
1354
+
},
1355
+
"node_modules/ansi-styles": {
1356
+
"version": "4.3.0",
1357
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
1358
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1359
+
"license": "MIT",
1360
+
"dependencies": {
1361
+
"color-convert": "^2.0.1"
1362
+
},
1363
+
"engines": {
1364
+
"node": ">=8"
1365
+
},
1366
+
"funding": {
1367
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
1368
+
}
1369
+
},
1321
1370
"node_modules/aria-query": {
1322
1371
"version": "5.3.2",
1323
1372
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
1324
1373
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
1325
-
"dev": true,
1326
1374
"license": "Apache-2.0",
1327
1375
"engines": {
1328
1376
"node": ">= 0.4"
···
1338
1386
"version": "4.1.0",
1339
1387
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
1340
1388
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
1341
-
"dev": true,
1342
1389
"license": "Apache-2.0",
1343
1390
"engines": {
1344
1391
"node": ">= 0.4"
1345
1392
}
1346
1393
},
1394
+
"node_modules/camelcase": {
1395
+
"version": "5.3.1",
1396
+
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
1397
+
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
1398
+
"license": "MIT",
1399
+
"engines": {
1400
+
"node": ">=6"
1401
+
}
1402
+
},
1347
1403
"node_modules/chokidar": {
1348
1404
"version": "4.0.3",
1349
1405
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
···
1360
1416
"url": "https://paulmillr.com/funding/"
1361
1417
}
1362
1418
},
1419
+
"node_modules/cliui": {
1420
+
"version": "6.0.0",
1421
+
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
1422
+
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
1423
+
"license": "ISC",
1424
+
"dependencies": {
1425
+
"string-width": "^4.2.0",
1426
+
"strip-ansi": "^6.0.0",
1427
+
"wrap-ansi": "^6.2.0"
1428
+
}
1429
+
},
1363
1430
"node_modules/clsx": {
1364
1431
"version": "2.1.1",
1365
1432
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1366
1433
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1367
-
"dev": true,
1368
1434
"license": "MIT",
1369
1435
"engines": {
1370
1436
"node": ">=6"
1371
1437
}
1372
1438
},
1439
+
"node_modules/color-convert": {
1440
+
"version": "2.0.1",
1441
+
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1442
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1443
+
"license": "MIT",
1444
+
"dependencies": {
1445
+
"color-name": "~1.1.4"
1446
+
},
1447
+
"engines": {
1448
+
"node": ">=7.0.0"
1449
+
}
1450
+
},
1451
+
"node_modules/color-name": {
1452
+
"version": "1.1.4",
1453
+
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1454
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1455
+
"license": "MIT"
1456
+
},
1373
1457
"node_modules/cookie": {
1374
1458
"version": "0.6.0",
1375
1459
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
···
1398
1482
}
1399
1483
}
1400
1484
},
1485
+
"node_modules/decamelize": {
1486
+
"version": "1.2.0",
1487
+
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
1488
+
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
1489
+
"license": "MIT",
1490
+
"engines": {
1491
+
"node": ">=0.10.0"
1492
+
}
1493
+
},
1401
1494
"node_modules/deepmerge": {
1402
1495
"version": "4.3.1",
1403
1496
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
···
1425
1518
"dev": true,
1426
1519
"license": "MIT"
1427
1520
},
1521
+
"node_modules/dijkstrajs": {
1522
+
"version": "1.0.3",
1523
+
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
1524
+
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
1525
+
"license": "MIT"
1526
+
},
1527
+
"node_modules/emoji-regex": {
1528
+
"version": "8.0.0",
1529
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1530
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1531
+
"license": "MIT"
1532
+
},
1428
1533
"node_modules/enhanced-resolve": {
1429
1534
"version": "5.18.3",
1430
1535
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
···
1485
1590
"version": "1.2.2",
1486
1591
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
1487
1592
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
1488
-
"dev": true,
1489
1593
"license": "MIT"
1490
1594
},
1491
1595
"node_modules/esrap": {
1492
1596
"version": "2.1.3",
1493
1597
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.3.tgz",
1494
1598
"integrity": "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==",
1495
-
"dev": true,
1496
1599
"license": "MIT",
1497
1600
"dependencies": {
1498
1601
"@jridgewell/sourcemap-codec": "^1.4.15"
···
1516
1619
}
1517
1620
}
1518
1621
},
1622
+
"node_modules/find-up": {
1623
+
"version": "4.1.0",
1624
+
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
1625
+
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
1626
+
"license": "MIT",
1627
+
"dependencies": {
1628
+
"locate-path": "^5.0.0",
1629
+
"path-exists": "^4.0.0"
1630
+
},
1631
+
"engines": {
1632
+
"node": ">=8"
1633
+
}
1634
+
},
1519
1635
"node_modules/fsevents": {
1520
1636
"version": "2.3.3",
1521
1637
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
···
1531
1647
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1532
1648
}
1533
1649
},
1650
+
"node_modules/get-caller-file": {
1651
+
"version": "2.0.5",
1652
+
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
1653
+
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
1654
+
"license": "ISC",
1655
+
"engines": {
1656
+
"node": "6.* || 8.* || >= 10.*"
1657
+
}
1658
+
},
1534
1659
"node_modules/graceful-fs": {
1535
1660
"version": "4.2.11",
1536
1661
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
···
1538
1663
"dev": true,
1539
1664
"license": "ISC"
1540
1665
},
1666
+
"node_modules/is-fullwidth-code-point": {
1667
+
"version": "3.0.0",
1668
+
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1669
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1670
+
"license": "MIT",
1671
+
"engines": {
1672
+
"node": ">=8"
1673
+
}
1674
+
},
1541
1675
"node_modules/is-reference": {
1542
1676
"version": "3.0.3",
1543
1677
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
1544
1678
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
1545
-
"dev": true,
1546
1679
"license": "MIT",
1547
1680
"dependencies": {
1548
1681
"@types/estree": "^1.0.6"
···
1839
1972
"version": "3.0.0",
1840
1973
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
1841
1974
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
1842
-
"dev": true,
1843
1975
"license": "MIT"
1844
1976
},
1977
+
"node_modules/locate-path": {
1978
+
"version": "5.0.0",
1979
+
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
1980
+
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
1981
+
"license": "MIT",
1982
+
"dependencies": {
1983
+
"p-locate": "^4.1.0"
1984
+
},
1985
+
"engines": {
1986
+
"node": ">=8"
1987
+
}
1988
+
},
1845
1989
"node_modules/magic-string": {
1846
1990
"version": "0.30.21",
1847
1991
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
1848
1992
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
1849
-
"dev": true,
1850
1993
"license": "MIT",
1851
1994
"dependencies": {
1852
1995
"@jridgewell/sourcemap-codec": "^1.5.5"
···
1904
2047
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1905
2048
}
1906
2049
},
2050
+
"node_modules/p-limit": {
2051
+
"version": "2.3.0",
2052
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
2053
+
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
2054
+
"license": "MIT",
2055
+
"dependencies": {
2056
+
"p-try": "^2.0.0"
2057
+
},
2058
+
"engines": {
2059
+
"node": ">=6"
2060
+
},
2061
+
"funding": {
2062
+
"url": "https://github.com/sponsors/sindresorhus"
2063
+
}
2064
+
},
2065
+
"node_modules/p-locate": {
2066
+
"version": "4.1.0",
2067
+
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
2068
+
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
2069
+
"license": "MIT",
2070
+
"dependencies": {
2071
+
"p-limit": "^2.2.0"
2072
+
},
2073
+
"engines": {
2074
+
"node": ">=8"
2075
+
}
2076
+
},
2077
+
"node_modules/p-try": {
2078
+
"version": "2.2.0",
2079
+
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
2080
+
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
2081
+
"license": "MIT",
2082
+
"engines": {
2083
+
"node": ">=6"
2084
+
}
2085
+
},
2086
+
"node_modules/path-exists": {
2087
+
"version": "4.0.0",
2088
+
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
2089
+
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
2090
+
"license": "MIT",
2091
+
"engines": {
2092
+
"node": ">=8"
2093
+
}
2094
+
},
1907
2095
"node_modules/picocolors": {
1908
2096
"version": "1.1.1",
1909
2097
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
···
1925
2113
"url": "https://github.com/sponsors/jonschlinkert"
1926
2114
}
1927
2115
},
2116
+
"node_modules/pngjs": {
2117
+
"version": "5.0.0",
2118
+
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
2119
+
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
2120
+
"license": "MIT",
2121
+
"engines": {
2122
+
"node": ">=10.13.0"
2123
+
}
2124
+
},
1928
2125
"node_modules/postcss": {
1929
2126
"version": "8.5.6",
1930
2127
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
···
1982
2179
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
1983
2180
}
1984
2181
},
2182
+
"node_modules/qrcode": {
2183
+
"version": "1.5.4",
2184
+
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
2185
+
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
2186
+
"license": "MIT",
2187
+
"dependencies": {
2188
+
"dijkstrajs": "^1.0.1",
2189
+
"pngjs": "^5.0.0",
2190
+
"yargs": "^15.3.1"
2191
+
},
2192
+
"bin": {
2193
+
"qrcode": "bin/qrcode"
2194
+
},
2195
+
"engines": {
2196
+
"node": ">=10.13.0"
2197
+
}
2198
+
},
1985
2199
"node_modules/readdirp": {
1986
2200
"version": "4.1.2",
1987
2201
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
···
1996
2210
"url": "https://paulmillr.com/funding/"
1997
2211
}
1998
2212
},
2213
+
"node_modules/require-directory": {
2214
+
"version": "2.1.1",
2215
+
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
2216
+
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
2217
+
"license": "MIT",
2218
+
"engines": {
2219
+
"node": ">=0.10.0"
2220
+
}
2221
+
},
2222
+
"node_modules/require-main-filename": {
2223
+
"version": "2.0.0",
2224
+
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
2225
+
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
2226
+
"license": "ISC"
2227
+
},
1999
2228
"node_modules/rollup": {
2000
2229
"version": "4.53.3",
2001
2230
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
···
2051
2280
"node": ">=6"
2052
2281
}
2053
2282
},
2283
+
"node_modules/set-blocking": {
2284
+
"version": "2.0.0",
2285
+
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
2286
+
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
2287
+
"license": "ISC"
2288
+
},
2054
2289
"node_modules/set-cookie-parser": {
2055
2290
"version": "2.7.2",
2056
2291
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
···
2083
2318
"node": ">=0.10.0"
2084
2319
}
2085
2320
},
2321
+
"node_modules/string-width": {
2322
+
"version": "4.2.3",
2323
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2324
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2325
+
"license": "MIT",
2326
+
"dependencies": {
2327
+
"emoji-regex": "^8.0.0",
2328
+
"is-fullwidth-code-point": "^3.0.0",
2329
+
"strip-ansi": "^6.0.1"
2330
+
},
2331
+
"engines": {
2332
+
"node": ">=8"
2333
+
}
2334
+
},
2335
+
"node_modules/strip-ansi": {
2336
+
"version": "6.0.1",
2337
+
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2338
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2339
+
"license": "MIT",
2340
+
"dependencies": {
2341
+
"ansi-regex": "^5.0.1"
2342
+
},
2343
+
"engines": {
2344
+
"node": ">=8"
2345
+
}
2346
+
},
2086
2347
"node_modules/svelte": {
2087
2348
"version": "5.43.15",
2088
2349
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.15.tgz",
2089
2350
"integrity": "sha512-FYlfm3oyLBNUy2NGqaWfKPiGOamS6YB8BJwAcF9xSXVFUjfcl9Ded1YSMu1vXEf0y0lcmBj45UgnOY2ZxhW0Cw==",
2090
-
"dev": true,
2091
2351
"license": "MIT",
2092
2352
"peer": true,
2093
2353
"dependencies": {
···
2239
2499
"multiformats": "^9.4.2"
2240
2500
}
2241
2501
},
2502
+
"node_modules/undici-types": {
2503
+
"version": "7.16.0",
2504
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
2505
+
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
2506
+
"dev": true,
2507
+
"license": "MIT"
2508
+
},
2242
2509
"node_modules/unicode-segmenter": {
2243
2510
"version": "0.14.0",
2244
2511
"resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.0.tgz",
···
2341
2608
}
2342
2609
}
2343
2610
},
2611
+
"node_modules/which-module": {
2612
+
"version": "2.0.1",
2613
+
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
2614
+
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
2615
+
"license": "ISC"
2616
+
},
2617
+
"node_modules/wrap-ansi": {
2618
+
"version": "6.2.0",
2619
+
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
2620
+
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
2621
+
"license": "MIT",
2622
+
"dependencies": {
2623
+
"ansi-styles": "^4.0.0",
2624
+
"string-width": "^4.1.0",
2625
+
"strip-ansi": "^6.0.0"
2626
+
},
2627
+
"engines": {
2628
+
"node": ">=8"
2629
+
}
2630
+
},
2631
+
"node_modules/y18n": {
2632
+
"version": "4.0.3",
2633
+
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
2634
+
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
2635
+
"license": "ISC"
2636
+
},
2637
+
"node_modules/yargs": {
2638
+
"version": "15.4.1",
2639
+
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
2640
+
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
2641
+
"license": "MIT",
2642
+
"dependencies": {
2643
+
"cliui": "^6.0.0",
2644
+
"decamelize": "^1.2.0",
2645
+
"find-up": "^4.1.0",
2646
+
"get-caller-file": "^2.0.1",
2647
+
"require-directory": "^2.1.1",
2648
+
"require-main-filename": "^2.0.0",
2649
+
"set-blocking": "^2.0.0",
2650
+
"string-width": "^4.2.0",
2651
+
"which-module": "^2.0.0",
2652
+
"y18n": "^4.0.0",
2653
+
"yargs-parser": "^18.1.2"
2654
+
},
2655
+
"engines": {
2656
+
"node": ">=8"
2657
+
}
2658
+
},
2659
+
"node_modules/yargs-parser": {
2660
+
"version": "18.1.3",
2661
+
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
2662
+
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
2663
+
"license": "ISC",
2664
+
"dependencies": {
2665
+
"camelcase": "^5.0.0",
2666
+
"decamelize": "^1.2.0"
2667
+
},
2668
+
"engines": {
2669
+
"node": ">=6"
2670
+
}
2671
+
},
2344
2672
"node_modules/zimmerframe": {
2345
2673
"version": "1.1.4",
2346
2674
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
2347
2675
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
2348
-
"dev": true,
2349
2676
"license": "MIT"
2350
2677
},
2351
2678
"node_modules/zod": {
+3
package.json
+3
package.json
···
16
16
},
17
17
"dependencies": {
18
18
"@atproto/api": "^0.18.1",
19
+
"@lucide/svelte": "^0.555.0",
20
+
"qrcode": "^1.5.4",
19
21
"tldts": "^7.0.19"
20
22
},
21
23
"devDependencies": {
···
23
25
"@sveltejs/kit": "^2.49.0",
24
26
"@sveltejs/vite-plugin-svelte": "^6.2.1",
25
27
"@tailwindcss/vite": "^4.0.0",
28
+
"@types/qrcode": "^1.5.6",
26
29
"prettier": "^3.6.2",
27
30
"prettier-plugin-svelte": "^3.4.0",
28
31
"svelte": "^5.43.14",
+1
-1
scripts/test-config.js
+1
-1
scripts/test-config.js
+15
src/app.css
+15
src/app.css
···
72
72
background-color: rgb(var(--color-background));
73
73
color: rgb(var(--color-text-primary));
74
74
min-height: 100vh;
75
+
transition:
76
+
background-color 0.3s ease,
77
+
color 0.3s ease;
75
78
}
76
79
77
80
body {
···
79
82
color: rgb(var(--color-text-primary));
80
83
min-height: 100vh;
81
84
}
85
+
86
+
/* Smooth transitions for color changes */
87
+
* {
88
+
transition-property: background-color, border-color, color, fill, stroke;
89
+
transition-duration: 0.2s;
90
+
transition-timing-function: ease;
91
+
}
92
+
93
+
/* Preserve transform transitions */
94
+
*:where([style*='transform']) {
95
+
transition-property: background-color, border-color, color, fill, stroke, transform;
96
+
}
+57
src/lib/components/ApiEndpoint.svelte
+57
src/lib/components/ApiEndpoint.svelte
···
1
+
<script lang="ts">
2
+
import { Globe } from '@lucide/svelte';
3
+
import CodeBlock from './CodeBlock.svelte';
4
+
5
+
interface Props {
6
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
7
+
path: string;
8
+
description: string;
9
+
href?: string;
10
+
}
11
+
12
+
let { method, path, description, href }: Props = $props();
13
+
14
+
let isHovered = $state(false);
15
+
16
+
const methodColors = {
17
+
GET: 'rgb(var(--color-success))',
18
+
POST: 'rgb(var(--color-primary))',
19
+
PUT: 'rgb(var(--color-primary))',
20
+
DELETE: 'rgb(var(--color-error))'
21
+
};
22
+
</script>
23
+
24
+
<li class="rounded-lg p-3" style="background-color: rgb(var(--color-surface))">
25
+
{#if href}
26
+
<a
27
+
{href}
28
+
class="flex items-center gap-4 no-underline"
29
+
style="color: rgb(var(--color-text-primary))"
30
+
onmouseenter={() => (isHovered = true)}
31
+
onmouseleave={() => (isHovered = false)}
32
+
>
33
+
<span
34
+
class="shrink-0 rounded px-2 py-1 text-xs font-bold"
35
+
style="background-color: {methodColors[method]}; color: white"
36
+
>
37
+
{method}
38
+
</span>
39
+
<CodeBlock>{path}</CodeBlock>
40
+
<span style="color: rgb(var(--color-text-secondary))">{description}</span>
41
+
{#if isHovered}
42
+
<Globe size={16} class="ml-auto shrink-0" style="color: rgb(var(--color-text-tertiary))" />
43
+
{/if}
44
+
</a>
45
+
{:else}
46
+
<div class="flex items-center gap-4" style="color: rgb(var(--color-text-primary))">
47
+
<span
48
+
class="shrink-0 rounded px-2 py-1 text-xs font-bold"
49
+
style="background-color: {methodColors[method]}; color: white"
50
+
>
51
+
{method}
52
+
</span>
53
+
<CodeBlock>{path}</CodeBlock>
54
+
<span style="color: rgb(var(--color-text-secondary))">{description}</span>
55
+
</div>
56
+
{/if}
57
+
</li>
+17
src/lib/components/CodeBlock.svelte
+17
src/lib/components/CodeBlock.svelte
···
1
+
<script lang="ts">
2
+
import { Code2 } from '@lucide/svelte';
3
+
4
+
interface Props {
5
+
children: import('svelte').Snippet;
6
+
}
7
+
8
+
let { children }: Props = $props();
9
+
</script>
10
+
11
+
<code
12
+
class="inline-flex items-center gap-1.5 rounded px-2 py-1 font-mono text-sm"
13
+
style="background-color: rgb(var(--color-code-bg))"
14
+
>
15
+
<Code2 size={14} style="color: rgb(var(--color-text-secondary))" />
16
+
{@render children()}
17
+
</code>
+44
src/lib/components/CopyButton.svelte
+44
src/lib/components/CopyButton.svelte
···
1
+
<script lang="ts">
2
+
import { Copy, Check } from '@lucide/svelte';
3
+
4
+
interface Props {
5
+
text: string;
6
+
size?: number;
7
+
}
8
+
9
+
let { text, size = 16 }: Props = $props();
10
+
11
+
let copied = $state(false);
12
+
let timeout: ReturnType<typeof setTimeout> | undefined;
13
+
14
+
async function copyToClipboard() {
15
+
try {
16
+
await navigator.clipboard.writeText(text);
17
+
copied = true;
18
+
19
+
// Clear existing timeout
20
+
if (timeout) clearTimeout(timeout);
21
+
22
+
// Reset after 2 seconds
23
+
timeout = setTimeout(() => {
24
+
copied = false;
25
+
}, 2000);
26
+
} catch (err) {
27
+
console.error('Failed to copy:', err);
28
+
}
29
+
}
30
+
</script>
31
+
32
+
<button
33
+
onclick={copyToClipboard}
34
+
class="rounded p-1 transition-colors hover:bg-opacity-20"
35
+
style="color: rgb(var(--color-text-secondary))"
36
+
aria-label={copied ? 'Copied!' : 'Copy to clipboard'}
37
+
title={copied ? 'Copied!' : 'Copy to clipboard'}
38
+
>
39
+
{#if copied}
40
+
<Check {size} style="color: rgb(var(--color-success))" />
41
+
{:else}
42
+
<Copy {size} />
43
+
{/if}
44
+
</button>
+28
src/lib/components/Link.svelte
+28
src/lib/components/Link.svelte
···
1
+
<script lang="ts">
2
+
import { ExternalLink } from '@lucide/svelte';
3
+
4
+
interface Props {
5
+
href: string;
6
+
children: import('svelte').Snippet;
7
+
external?: boolean;
8
+
}
9
+
10
+
let { href, children, external = false }: Props = $props();
11
+
12
+
let isHovered = $state(false);
13
+
</script>
14
+
15
+
<a
16
+
{href}
17
+
class="inline-flex items-center gap-1.5 no-underline transition-colors"
18
+
style="color: {isHovered ? 'rgb(var(--color-primary-hover))' : 'rgb(var(--color-primary))'}"
19
+
onmouseenter={() => (isHovered = true)}
20
+
onmouseleave={() => (isHovered = false)}
21
+
target={external ? '_blank' : undefined}
22
+
rel={external ? 'noopener noreferrer' : undefined}
23
+
>
24
+
{@render children()}
25
+
{#if external}
26
+
<ExternalLink size={14} class="shrink-0" />
27
+
{/if}
28
+
</a>
+107
src/lib/components/QRCodeModal.svelte
+107
src/lib/components/QRCodeModal.svelte
···
1
+
<script lang="ts">
2
+
import { X } from '@lucide/svelte';
3
+
import { onMount } from 'svelte';
4
+
5
+
interface Props {
6
+
url: string;
7
+
isOpen: boolean;
8
+
onClose: () => void;
9
+
}
10
+
11
+
let { url, isOpen, onClose }: Props = $props();
12
+
13
+
let qrCodeContainer: HTMLDivElement;
14
+
15
+
$effect(() => {
16
+
if (isOpen && qrCodeContainer && typeof window !== 'undefined') {
17
+
import('qrcode').then(({ default: QRCode }) => {
18
+
qrCodeContainer.innerHTML = '';
19
+
QRCode.toCanvas(url, {
20
+
errorCorrectionLevel: 'H',
21
+
margin: 2,
22
+
width: 256,
23
+
color: {
24
+
dark: '#000000',
25
+
light: '#FFFFFF'
26
+
}
27
+
}, (error: Error | null | undefined, canvas: HTMLCanvasElement) => {
28
+
if (error) {
29
+
console.error('QR Code generation error:', error);
30
+
return;
31
+
}
32
+
if (qrCodeContainer) {
33
+
qrCodeContainer.appendChild(canvas);
34
+
}
35
+
});
36
+
});
37
+
}
38
+
});
39
+
</script>
40
+
41
+
{#if isOpen}
42
+
<div
43
+
style="
44
+
position: fixed;
45
+
top: 0;
46
+
left: 0;
47
+
right: 0;
48
+
bottom: 0;
49
+
width: 100vw;
50
+
height: 100vh;
51
+
background-color: rgba(0, 0, 0, 0.75);
52
+
display: flex;
53
+
align-items: center;
54
+
justify-content: center;
55
+
padding: 1rem;
56
+
z-index: 9999;
57
+
"
58
+
onclick={onClose}
59
+
role="dialog"
60
+
aria-modal="true"
61
+
aria-labelledby="qr-modal-title"
62
+
>
63
+
<div
64
+
class="relative w-full max-w-md rounded-lg p-8 shadow-2xl"
65
+
style="background-color: rgb(var(--color-surface)); color: rgb(var(--color-text-primary))"
66
+
onclick={(e) => e.stopPropagation()}
67
+
role="document"
68
+
>
69
+
<button
70
+
onclick={onClose}
71
+
class="absolute right-4 top-4 rounded-lg p-2 transition-colors"
72
+
style="color: rgb(var(--color-text-secondary)); background-color: transparent;"
73
+
onmouseenter={(e) => {
74
+
e.currentTarget.style.backgroundColor = 'rgb(var(--color-surface-elevated))';
75
+
}}
76
+
onmouseleave={(e) => {
77
+
e.currentTarget.style.backgroundColor = 'transparent';
78
+
}}
79
+
aria-label="Close modal"
80
+
>
81
+
<X size={20} />
82
+
</button>
83
+
84
+
<h2
85
+
id="qr-modal-title"
86
+
class="mb-6 text-xl font-semibold"
87
+
style="color: rgb(var(--color-text-primary))"
88
+
>
89
+
QR Code
90
+
</h2>
91
+
92
+
<div class="flex flex-col items-center gap-4">
93
+
<div
94
+
bind:this={qrCodeContainer}
95
+
class="rounded-lg p-4"
96
+
style="background-color: white;"
97
+
></div>
98
+
<p
99
+
class="max-w-xs break-all text-center text-sm"
100
+
style="color: rgb(var(--color-text-secondary))"
101
+
>
102
+
{url}
103
+
</p>
104
+
</div>
105
+
</div>
106
+
</div>
107
+
{/if}
+15
src/lib/components/Section.svelte
+15
src/lib/components/Section.svelte
···
1
+
<script lang="ts">
2
+
interface Props {
3
+
title: string;
4
+
children: import('svelte').Snippet;
5
+
}
6
+
7
+
let { title, children }: Props = $props();
8
+
</script>
9
+
10
+
<section class="mb-12">
11
+
<h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))">
12
+
{title}
13
+
</h2>
14
+
{@render children()}
15
+
</section>
+72
src/lib/components/ShortLinkItem.svelte
+72
src/lib/components/ShortLinkItem.svelte
···
1
+
<script lang="ts">
2
+
import { Link as LinkIcon, ArrowRight, QrCode } from '@lucide/svelte';
3
+
import CodeBlock from './CodeBlock.svelte';
4
+
import CopyButton from './CopyButton.svelte';
5
+
import QRCodeModal from './QRCodeModal.svelte';
6
+
7
+
interface Props {
8
+
shortcode: string;
9
+
title: string;
10
+
emoji?: string;
11
+
}
12
+
13
+
let { shortcode, title, emoji }: Props = $props();
14
+
15
+
let isHovered = $state(false);
16
+
let showQRModal = $state(false);
17
+
18
+
// Get the full URL for copying
19
+
const fullUrl = typeof window !== 'undefined' ? `${window.location.origin}/${shortcode}` : '';
20
+
</script>
21
+
22
+
<li>
23
+
<div
24
+
class="flex items-center gap-3 rounded-lg p-3 transition-all"
25
+
style="background-color: {isHovered
26
+
? 'rgb(var(--color-surface-elevated))'
27
+
: 'rgb(var(--color-surface))'}; color: rgb(var(--color-text-primary))"
28
+
role="group"
29
+
onmouseenter={() => (isHovered = true)}
30
+
onmouseleave={() => (isHovered = false)}
31
+
>
32
+
{#if emoji}
33
+
<span class="shrink-0 text-2xl">{emoji}</span>
34
+
{:else}
35
+
<LinkIcon size={20} class="shrink-0" style="color: rgb(var(--color-text-secondary))" />
36
+
{/if}
37
+
38
+
<a href="/{shortcode}" class="flex items-center gap-2 no-underline">
39
+
<CodeBlock>/{shortcode}</CodeBlock>
40
+
</a>
41
+
42
+
<span class="flex-1" style="color: rgb(var(--color-text-secondary))">{title}</span>
43
+
44
+
{#if fullUrl}
45
+
<button
46
+
onclick={() => (showQRModal = true)}
47
+
class="flex items-center gap-1 rounded-lg px-2 py-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
48
+
style="color: rgb(var(--color-text-tertiary))"
49
+
aria-label="Show QR code"
50
+
title="Show QR code"
51
+
>
52
+
<QrCode size={16} class="shrink-0" />
53
+
</button>
54
+
55
+
<CopyButton text={fullUrl} />
56
+
{/if}
57
+
58
+
<a
59
+
href="/{shortcode}"
60
+
class="flex items-center gap-1 no-underline transition-colors"
61
+
style="color: rgb(var(--color-text-tertiary))"
62
+
>
63
+
<ArrowRight
64
+
size={16}
65
+
class="shrink-0 transition-transform"
66
+
style="transform: translateX({isHovered ? '4px' : '0'})"
67
+
/>
68
+
</a>
69
+
</div>
70
+
</li>
71
+
72
+
<QRCodeModal url={fullUrl} isOpen={showQRModal} onClose={() => (showQRModal = false)} />
+32
src/lib/components/Spinner.svelte
+32
src/lib/components/Spinner.svelte
···
1
+
<script lang="ts">
2
+
import { Loader2 } from '@lucide/svelte';
3
+
4
+
interface Props {
5
+
size?: number;
6
+
text?: string;
7
+
}
8
+
9
+
let { size = 24, text }: Props = $props();
10
+
</script>
11
+
12
+
<div class="flex items-center gap-3" style="color: rgb(var(--color-text-secondary))">
13
+
<Loader2 {size} class="animate-spin" />
14
+
{#if text}
15
+
<span>{text}</span>
16
+
{/if}
17
+
</div>
18
+
19
+
<style>
20
+
@keyframes spin {
21
+
from {
22
+
transform: rotate(0deg);
23
+
}
24
+
to {
25
+
transform: rotate(360deg);
26
+
}
27
+
}
28
+
29
+
:global(.animate-spin) {
30
+
animation: spin 1s linear infinite;
31
+
}
32
+
</style>
+38
src/lib/components/StatusCard.svelte
+38
src/lib/components/StatusCard.svelte
···
1
+
<script lang="ts">
2
+
import { CheckCircle2, AlertCircle } from '@lucide/svelte';
3
+
4
+
interface Props {
5
+
type: 'success' | 'error';
6
+
children: import('svelte').Snippet;
7
+
}
8
+
9
+
let { type, children }: Props = $props();
10
+
11
+
const styles = {
12
+
success: {
13
+
bg: 'rgb(var(--color-success-bg))',
14
+
border: 'rgb(var(--color-success-border))',
15
+
icon: CheckCircle2
16
+
},
17
+
error: {
18
+
bg: 'rgb(var(--color-error-bg))',
19
+
border: 'rgb(var(--color-error-border))',
20
+
icon: AlertCircle
21
+
}
22
+
};
23
+
24
+
const config = styles[type];
25
+
const Icon = config.icon;
26
+
</script>
27
+
28
+
<div
29
+
class="mb-4 rounded-lg border p-4"
30
+
style="background-color: {config.bg}; border-color: {config.border}"
31
+
>
32
+
<div class="flex items-start gap-3">
33
+
<Icon size={24} class="shrink-0" style="color: rgb(var(--color-text-primary))" />
34
+
<div class="flex-1">
35
+
{@render children()}
36
+
</div>
37
+
</div>
38
+
</div>
+39
src/lib/components/ThemeToggle.svelte
+39
src/lib/components/ThemeToggle.svelte
···
1
+
<script lang="ts">
2
+
import { Sun, Moon } from '@lucide/svelte';
3
+
import { onMount } from 'svelte';
4
+
5
+
let isDark = $state(false);
6
+
let mounted = $state(false);
7
+
8
+
onMount(() => {
9
+
// Check initial theme
10
+
isDark = document.documentElement.classList.contains('dark');
11
+
mounted = true;
12
+
});
13
+
14
+
function toggleTheme() {
15
+
isDark = !isDark;
16
+
if (isDark) {
17
+
document.documentElement.classList.add('dark');
18
+
localStorage.setItem('theme', 'dark');
19
+
} else {
20
+
document.documentElement.classList.remove('dark');
21
+
localStorage.setItem('theme', 'light');
22
+
}
23
+
}
24
+
</script>
25
+
26
+
{#if mounted}
27
+
<button
28
+
onclick={toggleTheme}
29
+
class="fixed bottom-6 right-6 rounded-full p-3 shadow-lg transition-all hover:scale-110"
30
+
style="background-color: rgb(var(--color-surface-elevated)); color: rgb(var(--color-text-primary))"
31
+
aria-label="Toggle theme"
32
+
>
33
+
{#if isDark}
34
+
<Sun size={20} />
35
+
{:else}
36
+
<Moon size={20} />
37
+
{/if}
38
+
</button>
39
+
{/if}
+9
src/lib/components/index.ts
+9
src/lib/components/index.ts
···
1
+
export { default as StatusCard } from './StatusCard.svelte';
2
+
export { default as CodeBlock } from './CodeBlock.svelte';
3
+
export { default as Link } from './Link.svelte';
4
+
export { default as ShortLinkItem } from './ShortLinkItem.svelte';
5
+
export { default as ApiEndpoint } from './ApiEndpoint.svelte';
6
+
export { default as Section } from './Section.svelte';
7
+
export { default as ThemeToggle } from './ThemeToggle.svelte';
8
+
export { default as Spinner } from './Spinner.svelte';
9
+
export { default as CopyButton } from './CopyButton.svelte';
+69
src/lib/components/types.ts
+69
src/lib/components/types.ts
···
1
+
/**
2
+
* Component prop types for the AT Protocol Shortlink application
3
+
*/
4
+
5
+
import type { Snippet } from 'svelte';
6
+
7
+
/**
8
+
* StatusCard component props
9
+
*/
10
+
export interface StatusCardProps {
11
+
type: 'success' | 'error';
12
+
children: Snippet;
13
+
}
14
+
15
+
/**
16
+
* CodeBlock component props
17
+
*/
18
+
export interface CodeBlockProps {
19
+
children: Snippet;
20
+
}
21
+
22
+
/**
23
+
* Link component props
24
+
*/
25
+
export interface LinkProps {
26
+
href: string;
27
+
children: Snippet;
28
+
external?: boolean;
29
+
}
30
+
31
+
/**
32
+
* ShortLinkItem component props
33
+
*/
34
+
export interface ShortLinkItemProps {
35
+
shortcode: string;
36
+
title: string;
37
+
emoji?: string;
38
+
}
39
+
40
+
/**
41
+
* ApiEndpoint component props
42
+
*/
43
+
export interface ApiEndpointProps {
44
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
45
+
path: string;
46
+
description: string;
47
+
href?: string;
48
+
}
49
+
50
+
/**
51
+
* Section component props
52
+
*/
53
+
export interface SectionProps {
54
+
title: string;
55
+
children: Snippet;
56
+
}
57
+
58
+
/**
59
+
* Spinner component props
60
+
*/
61
+
export interface SpinnerProps {
62
+
size?: number;
63
+
text?: string;
64
+
}
65
+
66
+
/**
67
+
* ThemeToggle component has no props
68
+
*/
69
+
export interface ThemeToggleProps {}
+32
-32
src/lib/utils/encoding.ts
+32
-32
src/lib/utils/encoding.ts
···
1
-
import { SHORTCODE } from '$lib/constants'; // Import constants for shortcode configuration
2
-
import { parse, getDomain } from 'tldts'; // Import utility functions for domain parsing
1
+
import { SHORTCODE } from '$lib/constants'; // Import constants for shortcode configuration
2
+
import { parse, getDomain } from 'tldts'; // Import utility functions for domain parsing
3
3
4
4
// Constants related to the character set for encoding
5
-
const BASE_CHARS = SHORTCODE.CHARS; // Charset for the shortcode encoding
6
-
const BASE = BASE_CHARS.length; // Base (the number of unique characters in the charset)
5
+
const BASE_CHARS = SHORTCODE.CHARS; // Charset for the shortcode encoding
6
+
const BASE = BASE_CHARS.length; // Base (the number of unique characters in the charset)
7
7
8
8
// Hashes a given string into a bigint value
9
9
function hashString(text: string): bigint {
10
-
let hash = 1469598103934665603n; // FNV-1a hash initialisation value
10
+
let hash = 1469598103934665603n; // FNV-1a hash initialisation value
11
11
for (let i = 0; i < text.length; i++) {
12
-
const char = BigInt(text.charCodeAt(i)); // Convert each character to a bigint
13
-
hash = (hash ^ char) * 1099511628211n; // FNV-1a hashing algorithm
12
+
const char = BigInt(text.charCodeAt(i)); // Convert each character to a bigint
13
+
hash = (hash ^ char) * 1099511628211n; // FNV-1a hashing algorithm
14
14
}
15
-
return hash < 0n ? -hash : hash; // Ensure the hash is positive
15
+
return hash < 0n ? -hash : hash; // Ensure the hash is positive
16
16
}
17
17
18
18
// Converts a number (bigint) into a base encoded string with a given length
19
19
function toBase(num: bigint, length: number, seed = ''): string {
20
-
let encoded = ''; // The resulting encoded string
20
+
let encoded = ''; // The resulting encoded string
21
21
let n = num;
22
22
for (let i = 0; i < length; i++) {
23
23
let rem: bigint;
···
30
30
const fallback = hashString(num.toString() + '::' + seed + '::' + i.toString());
31
31
rem = fallback % BigInt(BASE);
32
32
}
33
-
encoded = BASE_CHARS[Number(rem)] + encoded; // Prepend the character for this base value
33
+
encoded = BASE_CHARS[Number(rem)] + encoded; // Prepend the character for this base value
34
34
}
35
35
return encoded;
36
36
}
···
40
40
try {
41
41
// Ensure the URL starts with 'https://' and parse it
42
42
const parsed = new URL(url.startsWith('http') ? url : `https://${url}`);
43
-
parsed.hash = ''; // Remove hash fragment
43
+
parsed.hash = ''; // Remove hash fragment
44
44
45
45
// Sort URL query parameters alphabetically
46
46
const sortedParams = [...parsed.searchParams.entries()].sort((a, b) =>
47
47
a[0].localeCompare(b[0])
48
48
);
49
-
parsed.search = ''; // Clear existing search parameters
50
-
for (const [key, value] of sortedParams) parsed.searchParams.append(key, value); // Rebuild query string
49
+
parsed.search = ''; // Clear existing search parameters
50
+
for (const [key, value] of sortedParams) parsed.searchParams.append(key, value); // Rebuild query string
51
51
52
-
parsed.hostname = parsed.hostname.toLowerCase(); // Convert hostname to lowercase
53
-
parsed.protocol = 'https:'; // Ensure HTTPS protocol is used
54
-
return parsed.toString(); // Return the normalised URL as a string
52
+
parsed.hostname = parsed.hostname.toLowerCase(); // Convert hostname to lowercase
53
+
parsed.protocol = 'https:'; // Ensure HTTPS protocol is used
54
+
return parsed.toString(); // Return the normalised URL as a string
55
55
} catch (e) {
56
56
// If URL parsing fails, return the original URL (trimmed)
57
57
return url.trim();
···
79
79
// Validate and adjust the length of the shortcode
80
80
if (!Number.isInteger(length) || length < 3) length = SHORTCODE.DEFAULT_LENGTH;
81
81
82
-
const DOMAIN_PREFIX_LENGTH = 2; // Number of characters used for the domain prefix
82
+
const DOMAIN_PREFIX_LENGTH = 2; // Number of characters used for the domain prefix
83
83
84
84
// Normalise the URL and extract the base domain
85
85
const normalised = normaliseUrl(url);
···
92
92
// Calculate the remaining length for the URL core and tail
93
93
const remaining = Math.max(1, length - DOMAIN_PREFIX_LENGTH);
94
94
95
-
let hostname = ''; // The hostname portion of the URL
95
+
let hostname = ''; // The hostname portion of the URL
96
96
try {
97
-
hostname = new URL(normalised).hostname.toLowerCase(); // Try to extract hostname from normalised URL
97
+
hostname = new URL(normalised).hostname.toLowerCase(); // Try to extract hostname from normalised URL
98
98
} catch (e) {
99
99
// Fallback if URL parsing fails
100
100
try {
101
101
hostname = new URL(url.startsWith('http') ? url : `https://${url}`).hostname.toLowerCase();
102
102
} catch {
103
-
hostname = ''; // If both parsing attempts fail, leave hostname empty
103
+
hostname = ''; // If both parsing attempts fail, leave hostname empty
104
104
}
105
105
}
106
106
107
107
let subLevels: string[] = [];
108
108
// If there is a subdomain, split it into separate levels
109
109
if (apex && hostname && hostname !== apex) {
110
-
const sub = hostname.replace(new RegExp(`\.${apex}$`), ''); // Remove the apex domain
111
-
subLevels = sub.split('.'); // Split subdomains by '.'
110
+
const sub = hostname.replace(new RegExp(`\.${apex}$`), ''); // Remove the apex domain
111
+
subLevels = sub.split('.'); // Split subdomains by '.'
112
112
}
113
113
114
114
// URL core length is determined based on the remaining space after the domain prefix
115
115
const MIN_URL_CORE = 1;
116
116
const MIN_TAIL = 1;
117
-
const tailLength = remaining; // Length allocated to the tail portion of the shortcode
117
+
const tailLength = remaining; // Length allocated to the tail portion of the shortcode
118
118
119
119
// Hash the normalised URL for the URL core portion of the shortcode
120
120
const urlHash = hashString(normalised + '::url');
121
-
const urlCoreLength = remaining - subLevels.length; // Account for subdomain levels
121
+
const urlCoreLength = remaining - subLevels.length; // Account for subdomain levels
122
122
const urlCore = toBase(urlHash, Math.max(MIN_URL_CORE, urlCoreLength), 'url');
123
123
124
124
// Generate subdomain-based tail (if applicable)
125
125
const subTail: string[] = [];
126
-
const reversedSubLevels = subLevels.slice().reverse(); // Reverse the subdomain levels for encoding
126
+
const reversedSubLevels = subLevels.slice().reverse(); // Reverse the subdomain levels for encoding
127
127
for (let i = 0; i < reversedSubLevels.length; i++) {
128
-
const h = hashString(reversedSubLevels[i] + '::sub'); // Hash the subdomain level
129
-
subTail.push(toBase(h, 1, 'sub' + i)); // Add to subTail
128
+
const h = hashString(reversedSubLevels[i] + '::sub'); // Hash the subdomain level
129
+
subTail.push(toBase(h, 1, 'sub' + i)); // Add to subTail
130
130
}
131
131
132
132
// If no subdomain tail is generated, use a fallback hash for the tail
···
138
138
139
139
// Combine domain prefix, URL core, and tail to form the final shortcode
140
140
let out = domainPrefix + urlCore + tail;
141
-
if (out.length > length) out = out.slice(0, length); // Trim to the desired length
141
+
if (out.length > length) out = out.slice(0, length); // Trim to the desired length
142
142
if (out.length < length) {
143
143
// Pad the shortcode if it is too short
144
144
let pad = '';
···
148
148
pad += toBase(h, Math.min(4, length - out.length - pad.length), 'pad2' + i);
149
149
i++;
150
150
}
151
-
out += pad.slice(0, length - out.length); // Append padding to reach the correct length
151
+
out += pad.slice(0, length - out.length); // Append padding to reach the correct length
152
152
}
153
153
154
154
// --- LOGGING MAX COMBINATIONS --- (for debugging purposes)
155
-
const maxCombinations = BigInt(BASE) ** BigInt(length); // Calculate the max possible combinations for the shortcode
155
+
const maxCombinations = BigInt(BASE) ** BigInt(length); // Calculate the max possible combinations for the shortcode
156
156
console.log(`[Shortcode Info] URL: ${url}`);
157
157
console.log(`[Shortcode Info] Length: ${length}, Charset: ${BASE} chars`);
158
158
console.log(`[Shortcode Info] Max possible combinations: ${maxCombinations.toString()}`);
···
160
160
`[Shortcode Info] Domain prefix: ${domainPrefix}, URL core: ${urlCore}, Subdomain tail: ${tail}`
161
161
);
162
162
163
-
return out; // Return the final encoded shortcode
163
+
return out; // Return the final encoded shortcode
164
164
}
165
165
166
166
// Function to validate if a given shortcode is valid (contains only alphanumeric characters)
···
170
170
171
171
// Function to calculate the maximum number of possible combinations for a shortcode of a given length
172
172
export function getMaxCombinations(length: number): number {
173
-
return Math.pow(BASE, length); // BASE raised to the power of length
173
+
return Math.pow(BASE, length); // BASE raised to the power of length
174
174
}
+7
-1
src/routes/+layout.svelte
+7
-1
src/routes/+layout.svelte
···
1
1
<script lang="ts">
2
2
import '../app.css';
3
+
import { ThemeToggle } from '$lib/components';
3
4
4
5
let { children } = $props();
5
6
</script>
···
8
9
<script>
9
10
// Prevent flash of unstyled content (FOUC) by applying theme before page renders
10
11
(function () {
12
+
const stored = localStorage.getItem('theme');
11
13
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
12
14
const htmlElement = document.documentElement;
13
15
14
-
if (prefersDark) {
16
+
// Use stored theme if available, otherwise use system preference
17
+
const shouldBeDark = stored === 'dark' || (!stored && prefersDark);
18
+
19
+
if (shouldBeDark) {
15
20
htmlElement.classList.add('dark');
16
21
} else {
17
22
htmlElement.classList.remove('dark');
···
24
29
style="min-height: 100vh; background-color: rgb(var(--color-background)); color: rgb(var(--color-text-primary))"
25
30
>
26
31
{@render children()}
32
+
<ThemeToggle />
27
33
</div>
+76
-177
src/routes/+page.svelte
+76
-177
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
+
import { Github, Link as LinkIcon } from '@lucide/svelte';
3
+
import {
4
+
StatusCard,
5
+
CodeBlock,
6
+
Link,
7
+
ShortLinkItem,
8
+
ApiEndpoint,
9
+
Section
10
+
} from '$lib/components';
2
11
import type { PageData } from './$types';
3
12
4
13
export let data: PageData;
···
10
19
</svelte:head>
11
20
12
21
<main class="mx-auto max-w-3xl px-4 py-8 font-sans leading-relaxed">
13
-
<h1 class="mb-8 text-3xl font-bold" style="color: rgb(var(--color-text-primary))">
14
-
AT Protocol Link Shortener
15
-
</h1>
22
+
<header class="mb-8">
23
+
<h1 class="mb-2 text-3xl font-bold" style="color: rgb(var(--color-text-primary))">
24
+
AT Protocol Link Shortener
25
+
</h1>
26
+
<p style="color: rgb(var(--color-text-secondary))">
27
+
A self-hosted link shortening service powered by Linkat and AT Protocol
28
+
</p>
29
+
</header>
16
30
17
-
<section class="mb-12">
18
-
<h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))">
19
-
Service Status
20
-
</h2>
31
+
<Section title="Service Status">
21
32
{#if data.error}
22
-
<div
23
-
class="mb-4 rounded-lg border p-4"
24
-
style="background-color: rgb(var(--color-error-bg)); border-color: rgb(var(--color-error-border))"
25
-
>
26
-
<p class="font-bold" style="color: rgb(var(--color-text-primary))">
27
-
⚠️ Configuration Error
33
+
<StatusCard type="error">
34
+
<p class="mb-2 font-bold" style="color: rgb(var(--color-text-primary))">
35
+
Configuration Error
28
36
</p>
29
-
<p style="color: rgb(var(--color-text-primary))">{data.error}</p>
37
+
<p class="mb-2" style="color: rgb(var(--color-text-primary))">{data.error}</p>
38
+
30
39
{#if data.did === 'NOT_CONFIGURED'}
31
40
<div class="mt-4 border-t pt-4" style="border-color: rgb(var(--color-error-border))">
32
-
<p class="font-bold" style="color: rgb(var(--color-text-primary))">Quick Fix:</p>
33
-
<ol
34
-
class="ml-6 mt-2 list-decimal space-y-2 leading-8"
35
-
style="color: rgb(var(--color-text-primary))"
36
-
>
41
+
<p class="mb-3 font-bold" style="color: rgb(var(--color-text-primary))">Quick Fix:</p>
42
+
<ol class="ml-6 space-y-2" style="color: rgb(var(--color-text-primary))">
37
43
<li>
38
-
Create a <code
39
-
class="rounded px-2 py-1 font-mono text-sm"
40
-
style="background-color: rgb(var(--color-code-bg))">. env</code
41
-
> file in your project root
44
+
Create a <CodeBlock>.env</CodeBlock> file in your project root
42
45
</li>
43
46
<li>
44
-
Add: <code
45
-
class="rounded px-2 py-1 font-mono text-sm"
46
-
style="background-color: rgb(var(--color-code-bg))"
47
-
>ATPROTO_DID=did:plc:your-did-here</code
48
-
>
47
+
Add: <CodeBlock>ATPROTO_DID=did:plc:your-did-here</CodeBlock>
49
48
</li>
50
49
<li>
51
-
Find your DID at <a
52
-
href="https://pdsls.dev/"
53
-
target="_blank"
54
-
class="underline"
55
-
style="color: rgb(var(--color-error))">pdsls.dev</a
56
-
>
50
+
Find your DID at <Link href="https://pdsls.dev/" external>pdsls.dev</Link>
57
51
</li>
58
52
<li>
59
-
Run <code
60
-
class="rounded px-2 py-1 font-mono text-sm"
61
-
style="background-color: rgb(var(--color-code-bg))">npm run test:config</code
62
-
> to verify
53
+
Run <CodeBlock>npm run test:config</CodeBlock> to verify
63
54
</li>
64
55
<li>Restart the server</li>
65
56
</ol>
66
57
</div>
67
58
{/if}
68
-
</div>
59
+
</StatusCard>
69
60
{:else}
70
-
<div
71
-
class="mb-4 rounded-lg border p-4"
72
-
style="background-color: rgb(var(--color-success-bg)); border-color: rgb(var(--color-success-border))"
73
-
>
74
-
<p style="color: rgb(var(--color-text-primary))">✓ Service is running</p>
75
-
<p style="color: rgb(var(--color-text-primary))">
76
-
✓ Configured DID: <code
77
-
class="rounded px-2 py-1 font-mono text-sm"
78
-
style="background-color: rgb(var(--color-code-bg))">{data.did}</code
79
-
>
80
-
</p>
81
-
<p style="color: rgb(var(--color-text-primary))">✓ Active links: {data.linkCount}</p>
82
-
</div>
61
+
<StatusCard type="success">
62
+
<div class="space-y-1" style="color: rgb(var(--color-text-primary))">
63
+
<p>Service is running</p>
64
+
<p class="flex items-center gap-2">
65
+
Configured DID: <CodeBlock>{data.did}</CodeBlock>
66
+
</p>
67
+
<p>Active links: {data.linkCount}</p>
68
+
</div>
69
+
</StatusCard>
83
70
{/if}
84
-
</section>
71
+
</Section>
85
72
86
-
<section class="mb-12">
87
-
<h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))">
88
-
Available Short Links
89
-
</h2>
73
+
<Section title="Available Short Links">
90
74
{#if data.links && data.links.length > 0}
91
-
<ul class="m-0 list-none p-0">
75
+
<ul class="m-0 space-y-2 list-none p-0">
92
76
{#each data.links as link}
93
-
<li class="mb-2">
94
-
<a
95
-
href="/{link.shortcode}"
96
-
class="flex items-center gap-3 rounded-lg p-3 no-underline transition-colors"
97
-
style="background-color: rgb(var(--color-surface)); color: rgb(var(--color-text-primary))"
98
-
onmouseenter={(e) => {
99
-
e.currentTarget.style.backgroundColor = `rgb(var(--color-surface-elevated))`;
100
-
}}
101
-
onmouseleave={(e) => {
102
-
e.currentTarget.style.backgroundColor = `rgb(var(--color-surface))`;
103
-
}}
104
-
>
105
-
{#if link.emoji}
106
-
<span class="shrink-0 text-2xl">{link.emoji}</span>
107
-
{/if}
108
-
<code
109
-
class="rounded px-2 py-1 font-mono text-sm"
110
-
style="background-color: rgb(var(--color-code-bg))">/{link.shortcode}</code
111
-
>
112
-
<span style="color: rgb(var(--color-text-secondary))">{link.title}</span>
113
-
</a>
114
-
</li>
77
+
<ShortLinkItem shortcode={link.shortcode} title={link.title} emoji={link.emoji} />
115
78
{/each}
116
79
</ul>
117
80
{:else if !data.error}
118
-
<p class="italic" style="color: rgb(var(--color-text-secondary))">
119
-
No short links configured yet. Add links to your <a
120
-
href="https://linkat.blue"
121
-
target="_blank"
122
-
class="no-underline hover:underline"
123
-
style="color: rgb(var(--color-primary))"
124
-
onmouseenter={(e) => {
125
-
e.currentTarget.style.color = `rgb(var(--color-primary-hover))`;
126
-
}}
127
-
onmouseleave={(e) => {
128
-
e.currentTarget.style.color = `rgb(var(--color-primary))`;
129
-
}}>Linkat board</a
130
-
>!
131
-
</p>
81
+
<div
82
+
class="rounded-lg border p-6 text-center"
83
+
style="background-color: rgb(var(--color-surface)); border-color: rgb(var(--color-border))"
84
+
>
85
+
<LinkIcon size={48} class="mx-auto mb-4" style="color: rgb(var(--color-text-tertiary))" />
86
+
<p class="mb-2" style="color: rgb(var(--color-text-primary))">
87
+
No short links configured yet
88
+
</p>
89
+
<p style="color: rgb(var(--color-text-secondary))">
90
+
Add links to your <Link href="https://linkat.blue" external>Linkat board</Link> to get started!
91
+
</p>
92
+
</div>
132
93
{/if}
133
-
</section>
94
+
</Section>
134
95
135
-
<section class="mb-12">
136
-
<h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))">
137
-
API Endpoints
138
-
</h2>
139
-
<ul class="m-0 list-none p-0">
140
-
<li class="mb-3 rounded-lg p-3" style="background-color: rgb(var(--color-surface))">
141
-
<a
142
-
href="/api/links"
143
-
class="flex items-center gap-4 no-underline hover:underline"
144
-
style="color: rgb(var(--color-text-primary))"
145
-
>
146
-
<code
147
-
class="rounded px-2 py-1 font-mono text-sm"
148
-
style="background-color: rgb(var(--color-code-bg))">GET /api/links</code
149
-
>
150
-
<span style="color: rgb(var(--color-text-secondary))">List all short links (JSON)</span>
151
-
</a>
152
-
</li>
153
-
<li class="mb-3 rounded-lg p-3" style="background-color: rgb(var(--color-surface))">
154
-
<div class="flex items-center gap-4" style="color: rgb(var(--color-text-primary))">
155
-
<code
156
-
class="rounded px-2 py-1 font-mono text-sm"
157
-
style="background-color: rgb(var(--color-code-bg))">GET /:shortcode</code
158
-
>
159
-
<span style="color: rgb(var(--color-text-secondary))">Redirect to target URL (301)</span>
160
-
</div>
161
-
</li>
96
+
<Section title="API Endpoints">
97
+
<ul class="m-0 space-y-3 list-none p-0">
98
+
<ApiEndpoint
99
+
method="GET"
100
+
path="/api/links"
101
+
description="List all short links (JSON)"
102
+
href="/api/links"
103
+
/>
104
+
<ApiEndpoint method="GET" path="/:shortcode" description="Redirect to target URL (301)" />
162
105
</ul>
163
-
</section>
106
+
</Section>
164
107
165
108
<footer
166
-
class="mt-16 border-t pt-8 text-center text-sm"
109
+
class="mt-16 border-t pt-8 space-y-4 text-center text-sm"
167
110
style="border-color: rgb(var(--color-border)); color: rgb(var(--color-text-secondary))"
168
111
>
169
-
<p class="mb-2">
170
-
Powered by <a
171
-
href="https://linkat.blue"
172
-
target="_blank"
173
-
rel="noopener noreferrer"
174
-
class="no-underline hover:underline"
175
-
style="color: rgb(var(--color-primary))"
176
-
onmouseenter={(e) => {
177
-
e.currentTarget.style.color = `rgb(var(--color-primary-hover))`;
178
-
}}
179
-
onmouseleave={(e) => {
180
-
e.currentTarget.style.color = `rgb(var(--color-primary))`;
181
-
}}>Linkat</a
182
-
>,
183
-
<a
184
-
href="https://atproto.com"
185
-
target="_blank"
186
-
rel="noopener noreferrer"
187
-
class="no-underline hover:underline"
188
-
style="color: rgb(var(--color-primary))"
189
-
onmouseenter={(e) => {
190
-
e.currentTarget.style.color = `rgb(var(--color-primary-hover))`;
191
-
}}
192
-
onmouseleave={(e) => {
193
-
e.currentTarget.style.color = `rgb(var(--color-primary))`;
194
-
}}>AT Protocol</a
195
-
>, and
196
-
<a
197
-
href="https://slingshot.microcosm.blue"
198
-
target="_blank"
199
-
rel="noopener noreferrer"
200
-
class="no-underline hover:underline"
201
-
style="color: rgb(var(--color-primary))"
202
-
onmouseenter={(e) => {
203
-
e.currentTarget.style.color = `rgb(var(--color-primary-hover))`;
204
-
}}
205
-
onmouseleave={(e) => {
206
-
e.currentTarget.style.color = `rgb(var(--color-primary))`;
207
-
}}>Slingshot</a
208
-
>
112
+
<p>
113
+
Powered by
114
+
<Link href="https://linkat.blue" external>Linkat</Link>,
115
+
<Link href="https://atproto.com" external>AT Protocol</Link>, and
116
+
<Link href="https://slingshot.microcosm.blue" external>Slingshot</Link>
209
117
</p>
210
118
<p>
211
-
<a
212
-
href="https://github.com/ewanc26/atproto-shortlink"
213
-
target="_blank"
214
-
rel="noopener noreferrer"
215
-
class="no-underline hover:underline"
216
-
style="color: rgb(var(--color-primary))"
217
-
onmouseenter={(e) => {
218
-
e.currentTarget.style.color = `rgb(var(--color-primary-hover))`;
219
-
}}
220
-
onmouseleave={(e) => {
221
-
e.currentTarget.style.color = `rgb(var(--color-primary))`;
222
-
}}>Source Code on GitHub</a
223
-
>
119
+
<Link href="https://github.com/ewanc26/atproto-shortlink" external>
120
+
<Github size={14} />
121
+
Source Code on GitHub
122
+
</Link>
224
123
</p>
225
124
</footer>
226
125
</main>