Compare commits

...

163 Commits

Author SHA1 Message Date
Johannes Kirschbauer
7c8de49258 buildClan: remove in favor of lib.clan 2025-07-21 20:29:26 +02:00
Mic92
579492f071 Merge pull request 'migration guide: fix moon example' (#4423) from docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4423
2025-07-21 17:11:14 +00:00
Jörg Thalheim
0ed02da28f migration guide: fix moon example 2025-07-21 19:07:47 +02:00
Mic92
4abfbb05a2 Merge pull request 'extend migration guide' (#4421) from docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4421
2025-07-21 16:23:58 +00:00
Jörg Thalheim
6126cccbcc extend migration guide 2025-07-21 18:10:58 +02:00
brianmcgee
9e77d16e6d Merge pull request 'fix(ui): alignment issues with forms' (#4418) from ui/minor-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4418
2025-07-21 12:13:36 +00:00
Brian McGee
53752d4a69 fix(ui): alignment issues with forms 2025-07-21 13:09:53 +01:00
DavHau
38955f763f clan default template: add inputs to specialArgs 2025-07-21 18:39:51 +07:00
brianmcgee
bd97896899 Merge pull request 'fix(ui): remove extra margin in modal title' (#4415) from ui/minor-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4415
2025-07-21 10:22:15 +00:00
Brian McGee
d6efeb3295 fix(ui): remove extra margin in modal title 2025-07-21 11:18:22 +01:00
Luis Hebendanz
e3247d9c36 Merge pull request 'Fix multiple bugs in 'clan networking' command' (#4389) from Qubasa/clan-core:deploy_network into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4389
2025-07-21 07:35:54 +00:00
Qubasa
4055508588 clan-lib: Add object_name to ClassSource and don't override __repr__ from NetworkTechnologyBase instead overwrite it in ClassSource 2025-07-21 14:25:01 +07:00
Qubasa
ff65dfc883 clanServices: change tor service to have "client" and "server" roles instead of just "default"
also improve error message when user forgot to update machine in clan
networking command
2025-07-21 14:25:01 +07:00
Qubasa
1f5ef04a61 clan-lib: Fix network.py missing vars generation and use import_with_source for better trace ability 2025-07-21 12:40:49 +07:00
Qubasa
89f0e90910 clan-lib: Init import_utils to add debug information to dynamically imported modules 2025-07-21 12:40:49 +07:00
Qubasa
137aa71529 clan-lib: Fix is_running of tor.py 2025-07-21 12:40:49 +07:00
Qubasa
4b5273fbc1 clanServices: Fix tor service not exposing SOCKS port 2025-07-21 12:40:49 +07:00
clan-bot
aed48be645 Merge pull request 'Update data-mesher' (#4414) from update-data-mesher into main 2025-07-21 05:16:44 +00:00
gitea-actions[bot]
5fdc9823d1 Update data-mesher 2025-07-21 05:00:49 +00:00
clan-bot
f6284a7ac2 Merge pull request 'Update treefmt-nix' (#4405) from update-treefmt-nix into main 2025-07-20 15:15:54 +00:00
gitea-actions[bot]
72473746ff Update treefmt-nix 2025-07-20 15:01:26 +00:00
hsjobeki
4b36b3e07c Merge pull request 'ui/scene: mock create machine modal for testing' (#4404) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4404
2025-07-19 16:23:56 +00:00
Johannes Kirschbauer
5a63eeed4e ui/scene: mock create machine modal for testing 2025-07-19 18:19:37 +02:00
Johannes Kirschbauer
ac96d67f09 components/modal: fix missing onClose call 2025-07-19 18:19:19 +02:00
Johannes Kirschbauer
d01342aa79 components/modal: add missing properties {mount, class} 2025-07-19 18:18:56 +02:00
Johannes Kirschbauer
2d404254da ui/scene: fix initBase visibility 2025-07-19 18:18:05 +02:00
Johannes Kirschbauer
71b69c1010 ui/scene: add promise based create machine callback" 2025-07-19 18:17:38 +02:00
Johannes Kirschbauer
f155c68efe ui/scene: fix animateToPosition 2025-07-19 18:16:53 +02:00
hsjobeki
e57741b60c Merge pull request 'ui/scene: clean up initBase' (#4403) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4403
2025-07-19 12:51:04 +00:00
Johannes Kirschbauer
c9cacfcf62 ui/scene: fix typing checks 2025-07-19 14:47:23 +02:00
Johannes Kirschbauer
2d937b80b1 ui/scene: clean up initBase 2025-07-19 14:40:32 +02:00
clan-bot
e8b91e63bc Merge pull request 'Update treefmt-nix' (#4402) from update-treefmt-nix into main 2025-07-19 10:17:05 +00:00
gitea-actions[bot]
a9d6fa7712 Update treefmt-nix 2025-07-19 10:01:30 +00:00
hsjobeki
65a23983c2 Merge pull request 'ui/scene: add loading splash screen' (#4400) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4400
2025-07-18 17:42:15 +00:00
Johannes Kirschbauer
c181400267 ui/scene: add loading splash screen 2025-07-18 19:37:06 +02:00
hsjobeki
e8ff0d1ad4 Merge pull request 'ui/render: optimize rendering, requestRenderIfNotRequested' (#4398) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4398
2025-07-18 17:36:44 +00:00
Johannes Kirschbauer
f9f8a947e2 ui/splash: add scene splash screen 2025-07-18 19:36:02 +02:00
Johannes Kirschbauer
c5b0154af7 ui/logos: add darknet-builder logo 2025-07-18 19:35:11 +02:00
brianmcgee
864742f05f Merge pull request 'feat(ui): add creating cube animation' (#4399) from ui/creating-animation into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4399
2025-07-18 16:39:08 +00:00
Brian McGee
38b043f625 feat(ui): add creating cube animation 2025-07-18 17:31:30 +01:00
Johannes Kirschbauer
174e66ef95 ui/render: optimize rendering, requestRenderIfNotRequested 2025-07-18 18:15:30 +02:00
hsjobeki
315049de20 Merge pull request 'ui/controls: replace manual listeners with mapControl' (#4397) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4397
2025-07-18 15:49:36 +00:00
Johannes Kirschbauer
2e577dbd1e ui/controls: replace manual listeners with mapControl 2025-07-18 17:45:53 +02:00
Mic92
a9b457e063 Merge pull request 'clanServices/wifi: handle multiple instances' (#4260) from nim65s/clan-core:multi-wifi into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4260
2025-07-18 15:19:24 +00:00
hsjobeki
4281770ec7 Merge pull request 'ui/scene: hook up api' (#4388) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4388
2025-07-18 15:15:41 +00:00
Johannes Kirschbauer
1bd950fa39 ui/scene: remove all unneded complexity to reduce complexity and improve performance 2025-07-18 17:12:09 +02:00
Johannes Kirschbauer
e37b61240b ui/routing: move scene down clans/:id" 2025-07-18 17:11:32 +02:00
Johannes Kirschbauer
23d2975bb5 ui/store: add methods for sceneData 2025-07-18 17:11:04 +02:00
Johannes Kirschbauer
d441d4c1c1 ui/hooks: add overloaded useClanUri 2025-07-18 17:10:39 +02:00
Mic92
840cb7e2cb Merge pull request 'nginx: drop recommendedZstdSettings' (#4396) from zstd into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4396
2025-07-18 14:23:52 +00:00
Jörg Thalheim
cf232e1002 nginx: drop recommendedZstdSettings
nixpkgs no longer recommends it.
2025-07-18 16:17:36 +02:00
Mic92
7414dc6e7e Merge pull request 'clan-app: fix x86_64-darwin build' (#4395) from darwin-build into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4395
2025-07-18 14:10:26 +00:00
Jörg Thalheim
d97f997349 clan-app: fix x86_64-darwin build 2025-07-18 16:06:12 +02:00
pinpox
0621ae1ca6 Merge pull request 'fix workfow' (#4393) from fix-clan-core-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4393
2025-07-18 13:37:56 +00:00
pinpox
992048e1b2 Fix update-clan-core-for-checks script
create-pr needs to use /bin/sh to work. This PR makes the script posix
compliant, replacing any bash specific features with plain sh
alternatives
2025-07-18 15:33:36 +02:00
Mic92
261cad7674 Merge pull request 'build x86_64-darwin on main every few hours' (#4392) from darwin-ci into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4392
2025-07-18 12:43:17 +00:00
Jörg Thalheim
a012e4b1af build x86_64-darwin on main every few hours 2025-07-18 14:39:07 +02:00
Guilhem Saurel
158b98ee05 clanServices/wifi: fix for multiple instances
Without this, `nix build .#checks.x86_64-linux.wifi` fails with:
```
error: The option `nodes.first.systemd.services.NetworkManager-setup-secrets.serviceConfig.ExecStart' has conflicting definition values:
- In `/nix/store/x0…45-source/clanServices/wifi/default.nix, via option mappedServices."self-@clan/wifi".roles.default.perInstance, via option nixosModule': <derivation wifi-secrets>
- In `/nix/store/x0…45-source/clanServices/wifi/default.nix, via option mappedServices."self-@clan/wifi".roles.default.perInstance, via option nixosModule': <derivation wifi-secrets>
Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.
```
2025-07-17 23:30:50 +02:00
Guilhem Saurel
14d367e50f clanServices/wifi: update test with a second instance 2025-07-17 23:30:47 +02:00
lassulus
48c575699e Merge pull request 'network module + CLI' (#4344) from networking into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4344
2025-07-17 13:36:53 +00:00
lassulus
60768cc537 Add networking module
This adds a (for now hidden) clan network command that exposes list,
ping, overview subcommands to get informations about configured
networks.
ClanServices can now use the exports to define network specific
information.

This is not the complete feature yet, as we are lacking more tests and
documentation, but merging this now makes it easier to iterate.
2025-07-17 15:23:08 +02:00
Johannes Kirschbauer
c26dff282b ui/queries: init queries folder 2025-07-17 13:49:16 +02:00
hsjobeki
5022f6f26c Merge pull request 'ui/clan: rework routing concept' (#4385) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4385
2025-07-17 11:39:33 +00:00
Johannes Kirschbauer
94b93074bc ui/query: add correct resource path 2025-07-17 13:35:50 +02:00
Johannes Kirschbauer
d962033236 ui/clan: rework routing concept 2025-07-17 10:54:48 +02:00
Johannes Kirschbauer
a548851245 ui/hooks: useMaybeClanUri init hook
Needed for pre-rendering the cube scene with clanURI = null
When it later receives a value scene will get populated without completely re-rendering
2025-07-17 10:51:32 +02:00
Johannes Kirschbauer
b32e61bb6d ui/app: wrap with query client povider to make api cached calls 2025-07-17 10:49:47 +02:00
Johannes Kirschbauer
e731322af3 ui/store: infer type from return arg 2025-07-17 10:49:12 +02:00
hsjobeki
fd21c6b4ee Merge pull request 'buildClan: Add deprecation warning' (#4384) from Qubasa/clan-core:migrate_away_buildClan into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4384
Reviewed-by: hsjobeki <hsjobeki@gmail.com>
2025-07-17 08:44:04 +00:00
Qubasa
5a86862f47 buildClan: Add deprecation warning 2025-07-17 15:32:12 +07:00
Michael Hoang
1d1a2563c3 Merge pull request 'flake: remove unnecessary follows for data-mesher' (#4383) from push-yzqmtrtrkkzt into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4383
2025-07-17 07:37:20 +00:00
Michael Hoang
4bc57980ff flake: remove unnecessary follows for data-mesher 2025-07-17 17:30:36 +10:00
Luis Hebendanz
3afd0c0971 Merge pull request 'inventory: Add missing default value for exports.instances and exports.machines' (#4382) from Qubasa/clan-core:fix_inv_missing_default into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4382
2025-07-17 06:17:37 +00:00
Qubasa
e6a6cb27ec inventory: Add missing default value for exports.instances and exports.machines 2025-07-17 13:10:30 +07:00
clan-bot
dcd78c5d84 Merge pull request 'Update disko' (#4381) from update-disko into main 2025-07-17 05:16:49 +00:00
gitea-actions[bot]
2a1ad66292 Update disko 2025-07-17 05:00:49 +00:00
brianmcgee
5d0d4404b8 Merge pull request 'chore: add a check for background.jpg' (#4380) from chore/stupid-jpg-check into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4380
2025-07-16 16:15:38 +00:00
Brian McGee
7b369c77b5 chore: add a check for background.jpg 2025-07-16 18:11:40 +02:00
hsjobeki
06b70a982b Merge pull request 'UI/cubes: extend cubes scene' (#4375) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4375
2025-07-16 15:20:27 +00:00
Johannes Kirschbauer
c9b1b0fb94 ui/cubes: align with design 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
66bdbb0959 ui/cubes: init story 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
752f030d03 ui/storybook: add all stories 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
8c7e93c92e UI/cubes: group logic to add more meshed 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
579885a6e2 cubes: scene extend 2025-07-16 17:12:09 +02:00
brianmcgee
45f7ebc0c9 Merge pull request 'feat: onboarding workflow' (#4379) from ui/onboarding-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4379
2025-07-16 15:10:04 +00:00
Brian McGee
997d675f8c feat: onboarding workflow 2025-07-16 17:04:34 +02:00
Mic92
65608ad401 Merge pull request 'Update data-mesher' (#4370) from update-data-mesher into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4370
2025-07-16 09:42:26 +00:00
gitea-actions[bot]
f46bf04b30 Update data-mesher 2025-07-16 09:30:40 +00:00
Mic92
d036f98cd4 Merge pull request 'clan-cli: Move flash.py to clan_lib/flash' (#4374) from Qubasa/clan-core:move_flash into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4374
2025-07-16 09:30:04 +00:00
DavHau
81df09a284 Merge pull request 'cleanup_install' (#4373) from Qubasa/clan-core:cleanup_install into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4373
2025-07-16 09:18:09 +00:00
Qubasa
a90cb56886 clan-cli: Move flash.py to clan_lib/flash 2025-07-16 15:29:18 +07:00
brianmcgee
af2ad09517 Merge pull request 'feat: ui/toolbar' (#4357) from ui/toolbar into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4357
2025-07-16 08:00:11 +00:00
Brian McGee
08ee06447b feat(ui): toolbar component 2025-07-16 09:55:11 +02:00
brianmcgee
b741340607 Merge pull request 'onboarding workflow' (#4366) from ui/onboarding-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4366
Reviewed-by: Mic92 <joerg@thalheim.io>
2025-07-16 07:17:26 +00:00
Qubasa
cfba97eee5 clan-cli: Reference HostKeyCheck literal instead of duplicating the list everywhere 2025-07-16 13:12:48 +07:00
Qubasa
fb4ccd1f63 clan-lib: Remove duplicate fields from installOptions and instead use them from Remote 2025-07-16 13:05:05 +07:00
Qubasa
2c4e688b0a clan-lib: Change BuildOn enum to Literal type. Literals can be translated better to typescript 2025-07-16 12:48:04 +07:00
Qubasa
f8a0943fbd clan-cli: Fix incorrect ipv6 check in check_machine_ssh_reachable 2025-07-16 12:34:30 +07:00
hsjobeki
9d61e550d5 Merge pull request 'cli: fix dot files not copied to $out in buildPythonApplication' (#4371) from pkgs-for into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4371
2025-07-15 21:44:19 +00:00
Johannes Kirschbauer
5742b88777 cli: fix dot files not copied $out in buildPythonApplication
File such as .envrc, .gitignore where not copied into the package and thus missing in all templates
2025-07-15 23:33:34 +02:00
hsjobeki
2ef3e4cac4 Merge pull request 'clanInternals: refactor configsPerSystem, minimize diff' (#4369) from pkgs-for into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4369
2025-07-15 20:04:10 +00:00
Johannes Kirschbauer
5fc98a9611 clanInternals: refactor configsPerSystem, minimize diff 2025-07-15 21:40:22 +02:00
Kenji Berthold
79922e57b2 Merge pull request 'pkgs/cli: Validate clan directory for update-hardware-config' (#4367) from kenji/ke-hardware-update-validation into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4367
2025-07-15 18:07:12 +00:00
hsjobeki
164cc4a455 Merge pull request 'revert bd3861c58056a847556c459ce420968044ce1459' (#4368) from hsjobeki-patch-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4368
2025-07-15 18:02:53 +00:00
hsjobeki
341f444fa0 revert bd3861c580
revert Merge pull request 'Remove clanModules/*' (#4202) from remove-modules into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4202

See: https://git.clan.lol/clan/clan-core/issues/4365

Not all modules are migrated.
If they are not migrated, we need to write migration docs and please display the link to the migration docs
2025-07-15 17:51:36 +00:00
a-kenji
a76bea3537 pkgs/cli: Validate clan directory for update-hardware-config 2025-07-15 19:11:07 +02:00
Mic92
9bb366cdd7 Merge pull request 'gitignore-images' (#4364) from gitignore-images into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4364
2025-07-15 15:03:31 +00:00
Brian McGee
9f582cd338 wip: onboarding workflow 2025-07-15 15:42:37 +01:00
Jörg Thalheim
028700b058 update-flake-inputs: email/user doesn't need to be configured 2025-07-15 16:19:45 +02:00
Jörg Thalheim
e8b111e229 run flake updates every 5 hours 2025-07-15 16:11:54 +02:00
Luis Hebendanz
1d8445a347 Merge pull request 'pkgs/cli: Fix ssh logging' (#4362) from kenji/ke-ssh-remove-debug into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4362
2025-07-15 13:00:00 +00:00
kenji
d84822ae39 Merge pull request 'pkgs/clan(templates): Add shell completions' (#4327) from kenji/ke-disko-shell into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4327
2025-07-15 12:58:59 +00:00
kenji
c2229b4da3 Merge pull request 'pkgs/cli: Add facts deprecation warning to clan facts help output' (#4329) from kenji/ke-facts-cli-warning into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4329
2025-07-15 12:43:19 +00:00
a-kenji
936290b01d pkgs/cli: Add facts deprecation warning to clan facts help output 2025-07-15 14:28:36 +02:00
clan-bot
d8dbdb4419 Merge pull request 'Update sops-nix' (#4361) from update-sops-nix into main 2025-07-15 12:28:25 +00:00
pinpox
bd3861c580 Merge pull request 'Remove clanModules/*' (#4202) from remove-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4202
2025-07-15 12:25:15 +00:00
pinpox
13d69bcd66 Merge pull request 'clanServices: users -> remove isNormalUser option, set automatically' (#4351) from Qubasa/clan-core:good_default_for_users into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4351
Reviewed-by: lassulus <clanlol@lassul.us>
Reviewed-by: pinpox <clan@pablo.tools>
2025-07-15 12:24:55 +00:00
a-kenji
e342996306 pkgs/cli: Fix ssh logging
Fix the ssh logging level.
Currently the ssh commands is printed every time on an ssh connection.
While seeing the command is useful, we should print this when running
clan with the `--debug` flag.
2025-07-15 14:20:40 +02:00
kenji
bfec09e652 Merge pull request 'pkgs/clan: Add clan validation to vars' (#4360) from kenji/ke-add-clan-validation into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4360
2025-07-15 12:14:18 +00:00
gitea-actions[bot]
42ac9f3579 Update sops-nix 2025-07-15 12:14:08 +00:00
a-kenji
8178c41c7b pkgs/clan: Add clan validation to vars
Add clan validation to vars and facts subcommmands
2025-07-15 14:01:41 +02:00
pinpox
2a50dadf84 fmt 2025-07-15 13:48:37 +02:00
kenji
143fbb929f Merge pull request 'pkgs/clan: Further unify clan flake validation' (#4358) from kenji/ke-non-clan-commands into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4358
2025-07-15 11:46:04 +00:00
pinpox
aeb555a320 Fix tests 2025-07-15 13:40:54 +02:00
pinpox
8caaaa5b8b wip 2025-07-15 13:17:34 +02:00
a-kenji
6347bb7f3a pkgs/clan: Further unify clan flake validation
Further unify clan flake validation and improve test coverage.
2025-07-15 13:03:49 +02:00
Mic92
9a7288df3d Merge pull request 'add images to gitignore' (#4355) from gitignore-images into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4355
2025-07-15 10:37:25 +00:00
kenji
ce0ff60ad3 Merge pull request 'pkgs/clan: Add flake validation to clan show' (#4352) from kenji/ke-non-clan-show into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4352
2025-07-15 10:30:19 +00:00
Jörg Thalheim
d76dc55325 add images to gitignore
images now needed to be added with `git add --force`.
This makes accidential commits of large files harder.
2025-07-15 12:30:04 +02:00
Qubasa
70c1648caf clanServices: users -> remove isNormalUser option, set automatically
nix fmt
2025-07-15 17:21:52 +07:00
a-kenji
2ddba36b17 pkgs/clan: Add flake validation to clan show 2025-07-15 12:04:23 +02:00
a-kenji
d4cb206e3e pkgs/cli: Add require_flake clan validation logic
Add a `require_flake` function that checks, if no argument is passed, if
we are in a clan directory.
If not will throw a helpful error.

Before `clan show`:

```
Traceback (most recent call last):
  File "/nix/store/8kb3l3yvz6svygnxdlrw5lmd3h3chc8a-clan-cli/bin/.clan-wrapped", line 9, in <module>
    sys.exit(main())
             ~~~~^^
  File "/nix/store/8kb3l3yvz6svygnxdlrw5lmd3h3chc8a-clan-cli/lib/python3.13/site-packages/clan_cli/cli.py", line 493, in main
    args.func(args)
    ~~~~~~~~~^^^^^^
  File "/nix/store/8kb3l3yvz6svygnxdlrw5lmd3h3chc8a-clan-cli/lib/python3.13/site-packages/clan_cli/clan/show.py", line 12, in show_command
    meta = get_clan_details(flake)
  File "/nix/store/8kb3l3yvz6svygnxdlrw5lmd3h3chc8a-clan-cli/lib/python3.13/site-packages/clan_lib/clan/get.py", line 22, in get_clan_details
    if flake.is_local and not flake.path.exists():
       ^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'is_local'
```

with `require_flake`:

```
No clan flake found in the current directory or its parents - Use the --flake flag to specify a clan flake path or URL
```
2025-07-15 12:01:20 +02:00
pinpox
0e53499f40 Remove clanModules 2025-07-15 11:53:32 +02:00
Mic92
1befc86308 Merge pull request 'Update disko' (#4347) from update-disko into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4347
2025-07-15 09:50:39 +00:00
gitea-actions[bot]
999ade9dc4 Update disko 2025-07-15 09:03:02 +00:00
Mic92
01ab9e5dbf Merge pull request 'update-flake-inputs: enable auto-merge' (#4346) from flakes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4346
2025-07-15 09:02:06 +00:00
Jörg Thalheim
2ef248a10e update-flake-inputs: enable auto-merge 2025-07-15 10:55:28 +02:00
Mic92
24493e8768 Merge pull request 'Update nixpkgs' (#4340) from update-nixpkgs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4340
2025-07-15 08:43:26 +00:00
DavHau
b91158f454 vars/interface: make type of dependencies configurable
One vars get lifted to the global scope, dependencies need to be structured differently, eg. categorized by instances
2025-07-15 13:41:05 +07:00
DavHau
66a6758db4 vars/interface: cleanup + don't use specialArgs for pkgs 2025-07-15 13:07:18 +07:00
DavHau
61df393c2d vars: reduce dependency on pkgs
pass pkgs only to generators submodule which is the only place where it is needed because of finalScript
2025-07-15 12:15:12 +07:00
gitea-actions[bot]
13458e4f58 Update nixpkgs 2025-07-14 18:45:03 +00:00
Mic92
dc3a41f403 Merge pull request 'update-flake-inputs: set gitea-token/github-token correctly' (#4339) from flakes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4339
2025-07-14 18:32:00 +00:00
Jörg Thalheim
a424f318e4 update-flake-inputs: set gitea-token/github-token correctly 2025-07-14 20:28:30 +02:00
Mic92
36adc38ec4 Merge pull request 'update-flake-inputs: drop gitea vars' (#4338) from flakes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4338
2025-07-14 15:45:21 +00:00
Jörg Thalheim
29581cd1f4 update-flake-inputs: drop gitea vars 2025-07-14 17:41:48 +02:00
Mic92
4523596eba Merge pull request 'drop renovate' (#4337) from merge-when-green-joerg into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4337
2025-07-14 15:41:00 +00:00
Jörg Thalheim
e97b06c410 drop renovate
we now use gitea actions for it.
2025-07-14 17:37:32 +02:00
Mic92
f79b3c2761 Merge pull request 'add new workflow to do flake updates' (#4336) from flakes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4336
2025-07-14 15:14:42 +00:00
Jörg Thalheim
a509e16627 add new workflow to do flake updates 2025-07-14 17:11:22 +02:00
Luis Hebendanz
e05f4380d4 Merge pull request 'clan-cli: Make 'clan ssh' read out the targetHost to connect to' (#4335) from Qubasa/clan-core:fix_clan_ssh into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4335
2025-07-14 13:57:45 +00:00
Qubasa
e8b5e2c2c5 clan-cli: Fixup clan install which depends on ssh_parseargs.
clan-cli: Remove --ssh-option for now, as it can't work in current state

clan-cli: Remove nix_config from test as its impure
2025-07-14 20:47:49 +07:00
Qubasa
9630b6dbe4 clan-cli: Make 'clan ssh' read out the targetHost to connect to 2025-07-14 19:35:48 +07:00
DavHau
1c2b72c6f0 vars: cleanup nix interface 2025-07-14 18:20:04 +07:00
pinpox
c49a7c8277 Merge pull request 'Remove clanModules dependencies from admin service' (#4237) from admin-no-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4237
2025-07-14 08:32:27 +00:00
pinpox
939f724878 Remove clanModules dependencies from admin service 2025-07-14 10:26:35 +02:00
Luis Hebendanz
8a56776032 Merge pull request 'Simplify flake.select logs, make logs readable again' (#4333) from Qubasa/clan-core:improve_log_output into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4333
2025-07-14 05:14:12 +00:00
Qubasa
60f7f8598b docs: Document new debug env vars in debugging.md 2025-07-14 12:11:16 +07:00
Qubasa
36282b92bc clan-cli: improve log messages further
nix fmt
2025-07-14 12:02:03 +07:00
Qubasa
0cf35480a2 clan-cli: Filter out flake select traces to improve debug log visibility 2025-07-14 11:51:35 +07:00
renovate[bot]
e875df5665 chore(deps): update data-mesher digest to 309e06f 2025-07-14 00:10:13 +00:00
hsjobeki
92d5ea82a7 Merge pull request 'deploy: add warning about disko.nix' (#4330) from docs-3 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4330
2025-07-13 19:29:52 +00:00
Johannes Kirschbauer
8f5bf1ff2a deploy: add warning about disko.nix 2025-07-13 21:26:07 +02:00
152 changed files with 6286 additions and 1917 deletions

View File

@@ -0,0 +1,20 @@
name: Build Clan App (Darwin)
on:
schedule:
# Run every 4 hours
- cron: "0 */4 * * *"
workflow_dispatch:
push:
branches:
- main
jobs:
build-clan-app-darwin:
runs-on: nix
steps:
- uses: actions/checkout@v4
- name: Build clan-app for x86_64-darwin
run: |
nix build .#packages.x86_64-darwin.clan-app --system x86_64-darwin --log-format bar-with-logs

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
#!/bin/sh
# Shared script for creating pull requests in Gitea workflows
set -euo pipefail
set -eu
# Required environment variables:
# - CI_BOT_TOKEN: Gitea bot token for authentication
@@ -8,22 +9,22 @@ set -euo pipefail
# - PR_TITLE: Title of the pull request
# - PR_BODY: Body/description of the pull request
if [[ -z "${CI_BOT_TOKEN:-}" ]]; then
if [ -z "${CI_BOT_TOKEN:-}" ]; then
echo "Error: CI_BOT_TOKEN is not set" >&2
exit 1
fi
if [[ -z "${PR_BRANCH:-}" ]]; then
if [ -z "${PR_BRANCH:-}" ]; then
echo "Error: PR_BRANCH is not set" >&2
exit 1
fi
if [[ -z "${PR_TITLE:-}" ]]; then
if [ -z "${PR_TITLE:-}" ]; then
echo "Error: PR_TITLE is not set" >&2
exit 1
fi
if [[ -z "${PR_BODY:-}" ]]; then
if [ -z "${PR_BODY:-}" ]; then
echo "Error: PR_BODY is not set" >&2
exit 1
fi
@@ -43,9 +44,12 @@ resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
}" \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
pr_number=$(echo "$resp" | jq -r '.number')
if ! pr_number=$(echo "$resp" | jq -r '.number'); then
echo "Error parsing response from pull request creation" >&2
exit 1
fi
if [[ "$pr_number" == "null" ]]; then
if [ "$pr_number" = "null" ]; then
echo "Error creating pull request:" >&2
echo "$resp" | jq . >&2
exit 1
@@ -64,12 +68,15 @@ while true; do
"delete_branch_after_merge": true
}' \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
msg=$(echo "$resp" | jq -r '.message')
if [[ "$msg" != "Please try again later" ]]; then
if ! msg=$(echo "$resp" | jq -r '.message'); then
echo "Error parsing merge response" >&2
exit 1
fi
if [ "$msg" != "Please try again later" ]; then
break
fi
echo "Retrying in 2 seconds..."
sleep 2
done
echo "Pull request #$pr_number merge initiated"
echo "Pull request #$pr_number merge initiated"

View File

@@ -0,0 +1,26 @@
name: Update Flake Inputs
on:
schedule:
# Run every 5 hours
- cron: "0 */5 * * *"
workflow_dispatch:
repository_dispatch:
jobs:
update-flake-inputs:
runs-on: nix
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update flake inputs
uses: Mic92/update-flake-inputs-gitea@main
with:
# Exclude private flakes and update-clan-core checks flake
exclude-patterns: "devFlake/private/flake.nix,checks/impure/flake.nix"
auto-merge: true
gitea-token: ${{ secrets.CI_BOT_TOKEN }}
github-token: ${{ secrets.CI_BOT_GITHUB_TOKEN }}

10
.gitignore vendored
View File

@@ -43,3 +43,13 @@ pkgs/clan-app/ui/api/Inventory.ts
pkgs/clan-app/ui/api/modules_schemas.json
pkgs/clan-app/ui/api/schema.json
pkgs/clan-app/ui/.fonts
# To avoid accidentally committing large files
# Can be added with `git add -f` after reviewing the filesize
# Large files should be avoided or stored externally i.e. a gitea release
*.jpg
*.png
*.jpeg
*.gif
*.mp4
*.mkv

View File

@@ -38,7 +38,6 @@
recommendedOptimisation = lib.mkDefault true;
recommendedProxySettings = lib.mkDefault true;
recommendedTlsSettings = lib.mkDefault true;
recommendedZstdSettings = lib.mkDefault true;
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
# instead of going to the journal!

View File

@@ -9,15 +9,37 @@
interface =
{ lib, ... }:
{
options.allowedKeys = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
description = "The allowed public keys for ssh access to the admin user";
example = {
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
options = {
allowedKeys = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
description = "The allowed public keys for ssh access to the admin user";
example = {
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
};
};
rsaHostKey.enable = lib.mkEnableOption "Generate RSA host key";
# TODO: allow per-server domains that we than collect in the inventory
#certicficateDomains = lib.mkOption {
# type = lib.types.listOf lib.types.str;
# default = [ ];
# example = [ "git.mydomain.com" ];
# description = "List of domains to include in the certificate. This option will not prepend the machine name in front of each domain.";
#};
certificateSearchDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "mydomain.com" ];
description = ''
List of domains to include in the certificate.
This option will prepend the machine name in front of each domain before adding it to the certificate.
'';
};
};
};
perInstance =
@@ -27,10 +49,15 @@
{ ... }:
{
imports = [
../../clanModules/sshd
../../clanModules/root-password
# We don't have a good way to specify dependencies between
# clanServices for now. When it get's implemtende, we should just
# use the ssh and users modules here.
./ssh.nix
./root-password.nix
];
_module.args = { inherit settings; };
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
};
};

View File

@@ -0,0 +1,39 @@
# We don't have a way of specifying dependencies between clanServices for now.
# When it get's added this file should be removed and the users module used instead.
{
config,
pkgs,
...
}:
{
users.mutableUsers = false;
users.users.root.hashedPasswordFile =
config.clan.core.vars.generators.root-password.files.password-hash.path;
clan.core.vars.generators.root-password = {
files.password-hash.neededFor = "users";
files.password.deploy = false;
runtimeInputs = [
pkgs.coreutils
pkgs.mkpasswd
pkgs.xkcdpass
];
prompts.password.type = "hidden";
prompts.password.persist = true;
prompts.password.description = "You can autogenerate a password, if you leave this prompt blank.";
script = ''
prompt_value="$(cat "$prompts"/password)"
if [[ -n "''${prompt_value-}" ]]; then
echo "$prompt_value" | tr -d "\n" > "$out"/password
else
xkcdpass --numwords 5 --delimiter - --count 1 | tr -d "\n" > "$out"/password
fi
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
'';
};
}

115
clanServices/admin/ssh.nix Normal file
View File

@@ -0,0 +1,115 @@
{
config,
pkgs,
lib,
settings,
...
}:
let
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
domains = stringSet settings.certificateSearchDomains;
in
{
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.HostCertificate = lib.mkIf (
settings.certificateSearchDomains != [ ]
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
hostKeys =
[
{
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
type = "ed25519";
}
]
++ lib.optional settings.rsaHostKey.enable {
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
type = "rsa";
};
};
clan.core.vars.generators.openssh = {
files."ssh.id_ed25519" = { };
files."ssh.id_ed25519.pub".secret = false;
migrateFact = "openssh";
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
'';
};
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
hostNames = [
"localhost"
config.networking.hostName
] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
};
clan.core.vars.generators.openssh-rsa = lib.mkIf settings.rsaHostKey.enable {
files."ssh.id_rsa" = { };
files."ssh.id_rsa.pub".secret = false;
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
'';
};
clan.core.vars.generators.openssh-cert = lib.mkIf (settings.certificateSearchDomains != [ ]) {
files."ssh.id_ed25519-cert.pub".secret = false;
dependencies = [
"openssh"
"openssh-ca"
];
validation = {
name = config.clan.core.settings.machine.name;
domains = lib.genAttrs settings.certificateSearchDomains lib.id;
};
runtimeInputs = [
pkgs.openssh
pkgs.jq
];
script = ''
ssh-keygen \
-s $in/openssh-ca/id_ed25519 \
-I ${config.clan.core.settings.machine.name} \
-h \
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
$in/openssh/ssh.id_ed25519.pub
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
'';
};
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
share = true;
files.id_ed25519.deploy = false;
files."id_ed25519.pub" = {
deploy = false;
secret = false;
};
runtimeInputs = [
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
'';
};
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
certAuthority = true;
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificateSearchDomains;
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
};
}

View File

@@ -0,0 +1,47 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/internet";
manifest.description = "direct access (or via ssh jumphost) to machines";
manifest.categories = [
"System"
"Network"
];
roles.default = {
interface =
{ lib, ... }:
{
options = {
host = lib.mkOption {
type = lib.types.str;
description = ''
ip address or hostname (domain) of the machine
'';
};
jumphosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
optional list of jumphosts to use to connect to the machine
'';
};
};
};
perInstance =
{
roles,
lib,
settings,
...
}:
{
exports.networking = {
# TODO add user space network support to clan-cli
peers = lib.mapAttrs (_name: machine: {
host.plain = machine.settings.host;
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
}) roles.default.machines;
};
};
};
}

View File

@@ -0,0 +1,9 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules = {
internet = module;
};
}

View File

@@ -0,0 +1,110 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/tor";
manifest.description = "Onion routing, use Hidden services to connect your machines";
manifest.categories = [
"System"
"Network"
];
roles.client = {
perInstance =
{
...
}:
{
nixosModule =
{
...
}:
{
config = {
services.tor = {
enable = true;
torsocks.enable = true;
client.enable = true;
};
};
};
};
};
roles.server = {
# interface =
# { lib, ... }:
# {
# options = {
# OciSettings = lib.mkOption {
# type = lib.types.raw;
# default = null;
# description = "NixOS settings for virtualisation.oci-container.<name>.settings";
# };
# buildContainer = lib.mkOption {
# type = lib.types.nullOr lib.types.str;
# default = null;
# };
# };
# };
perInstance =
{
instanceName,
roles,
lib,
...
}:
{
exports.networking = {
priority = lib.mkDefault 10;
# TODO add user space network support to clan-cli
module = "clan_lib.network.tor";
peers = lib.mapAttrs (name: machine: {
host.var = {
machine = name;
generator = "tor_${instanceName}";
file = "hostname";
};
}) roles.server.machines;
};
nixosModule =
{
pkgs,
config,
...
}:
{
config = {
services.tor = {
enable = true;
relay.onionServices."clan_${instanceName}" = {
version = 3;
# TODO get ports from instance machine config
map = [
{
port = 22;
target.port = 22;
}
];
secretKey = config.clan.core.vars.generators."tor_${instanceName}".files.hs_ed25519_secret_key.path;
};
};
clan.core.vars.generators."tor_${instanceName}" = {
files.hs_ed25519_secret_key = { };
files.hostname = { };
runtimeInputs = with pkgs; [
coreutils
tor
];
script = ''
mkdir -p data
echo -e "DataDirectory ./data\nSocksPort 0\nHiddenServiceDir ./hs\nHiddenServicePort 80 127.0.0.1:80" > torrc
timeout 2 tor -f torrc || :
mv hs/hs_ed25519_secret_key $out/hs_ed25519_secret_key
mv hs/hostname $out/hostname
'';
};
};
};
};
};
}

View File

@@ -0,0 +1,9 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules = {
tor = module;
};
}

View File

@@ -11,7 +11,7 @@
roles.default = {
interface =
{ config, lib, ... }:
{ lib, ... }:
{
options = {
user = lib.mkOption {
@@ -37,23 +37,6 @@
- `clan vars get <machine-name> <name-of-password-variable>`
'';
};
regularUser = lib.mkOption {
type = lib.types.bool;
default = config.user != "root";
defaultText = lib.literalExpression "config.user != \"root\"";
example = false;
description = ''
Whether the user should be a regular user or a system user.
Regular users are normal users that can log in and have a home directory.
System users are used for system services and do not have a home directory.
!!! Warning
`root` cannot be a regular user.
You must set this to `false` for `root`
'';
};
groups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
@@ -91,7 +74,7 @@
}:
{
users.users.${settings.user} = {
isNormalUser = settings.regularUser;
isNormalUser = if settings.user == "root" then false else true;
extraGroups = settings.groups;
hashedPasswordFile =

View File

@@ -39,7 +39,7 @@ in
};
perInstance =
{ settings, ... }:
{ instanceName, settings, ... }:
{
nixosModule =
{ pkgs, config, ... }:
@@ -86,7 +86,7 @@ in
# service to generate the environment file containing all secrets, as
# expected by the nixos NetworkManager-ensure-profile service
systemd.services.NetworkManager-setup-secrets = {
systemd.services."NetworkManager-setup-secrets-${instanceName}" = {
description = "Generate wifi secrets for NetworkManager";
requiredBy = [ "NetworkManager-ensure-profiles.service" ];
partOf = [ "NetworkManager-ensure-profiles.service" ];

View File

@@ -7,8 +7,16 @@
inventory = {
machines.test = { };
machines.second = { };
instances = {
wg-test-all = {
module.name = "@clan/wifi";
module.input = "self";
roles.default.tags.all = { };
roles.default.settings.networks.all = { };
};
wg-test-one = {
module.name = "@clan/wifi";
module.input = "self";

View File

@@ -11,7 +11,7 @@
...
}:
let
buildClanOptions = self'.legacyPackages.clan-internals-docs;
clanOptions = self'.legacyPackages.clan-internals-docs;
# Simply evaluated options (JSON)
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
@@ -99,7 +99,7 @@
# Frontmatter format for clanModules
export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json
export BUILD_CLAN_PATH=${buildClanOptions}/share/doc/nixos/options.json
export BUILD_CLAN_PATH=${clanOptions}/share/doc/nixos/options.json
mkdir $out

View File

@@ -465,6 +465,10 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
for module_name, module_info in service_links.items():
# Skip specific modules that are not ready for documentation
if module_name in ["internet", "tor"]:
continue
output = f"# {module_name}\n\n"
# output += f"`clan.modules.{module_name}`\n"
output += f"*{module_info['manifest']['description']}*\n"

View File

@@ -55,9 +55,37 @@ If you're using VSCode, it has a handy feature that makes paths to source code f
## Finding Print Messages
To identify where a specific print message comes from, you can enable a helpful feature. Simply set the environment variable `export TRACE_PRINT=1`. When you run commands with `--debug` mode, each print message will include information about its source location.
To trace the origin of print messages in `clan-cli`, you can enable special debugging features using environment variables:
- Set `TRACE_PRINT=1` to include the source location with each print message:
```bash
export TRACE_PRINT=1
```
When running commands with `--debug`, every print will show where it was triggered in the code.
- To see a deeper stack trace for each print, set `TRACE_DEPTH` to the desired number of stack frames (e.g., 3):
```bash
export TRACE_DEPTH=3
```
### Additional Debug Logging
You can enable more detailed logging for specific components by setting these environment variables:
- `CLAN_DEBUG_NIX_SELECTORS=1` — verbose logs for flake.select operations
- `CLAN_DEBUG_NIX_PREFETCH=1` — verbose logs for flake.prefetch operations
- `CLAN_DEBUG_COMMANDS=1` — print the diffed environment of executed commands
Example:
```bash
export CLAN_DEBUG_NIX_SELECTORS=1
export CLAN_DEBUG_NIX_PREFETCH=1
export CLAN_DEBUG_COMMANDS=1
```
These options help you pinpoint the source and context of print messages and debug logs during development.
If you need more details, you can expand the stack trace information that appears with each print by setting the environment variable `export TRACE_DEPTH=3`.
## Analyzing Performance

View File

@@ -181,6 +181,13 @@ You can have a look and customize it if needed.
!!! tip
For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
!!! Danger
Don't change the `disko.nix` after the machine is installed for the first time.
Changing disko configuration requires wiping and reinstalling the machine.
Unless you really know what you are doing.
## Deploy the machine
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your machine.
@@ -267,4 +274,3 @@ clan {
```
This is useful for machines that are not always online or are not part of the regular update cycle.

View File

@@ -35,6 +35,37 @@ services = {
};
```
### Complex Example: Multi-service Setup
```nix
# Old format
services = {
borgbackup.production = {
roles.server.machines = [ "backup-server" ];
roles.server.config = {
directory = "/var/backup/borg";
};
roles.client.tags = [ "backup" ];
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
};
zerotier.company-network = {
roles.controller.machines = [ "network-controller" ];
roles.moon.machines = [ "moon-1" "moon-2" ];
roles.peer.tags = [ "nixos" ];
};
sshd.internal = {
roles.server.tags = [ "nixos" ];
roles.client.tags = [ "nixos" ];
config.certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
```
---
## ✅ After: New `instances` Definition with `clanServices`
@@ -70,6 +101,56 @@ instances = {
};
```
### Complex Example Migrated
```nix
# New format
instances = {
borgbackup-production = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.server.machines."backup-server" = { };
roles.server.settings = {
directory = "/var/backup/borg";
};
roles.client.tags.backup = { };
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
};
zerotier-company-network = {
module = {
name = "zerotier";
input = "clan-core";
};
roles.controller.machines."network-controller" = { };
roles.moon.machines."moon-1".settings = {
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
};
roles.moon.machines."moon-2".settings = {
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
};
roles.peer.tags.nixos = { };
};
sshd-internal = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.nixos = { };
roles.client.tags.nixos = { };
roles.client.settings = {
certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
};
```
---
## Steps to Migrate
@@ -131,6 +212,33 @@ roles.default.machines."test-inventory-machine".settings = {
};
```
### Important Type Changes
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
```nix
# ❌ Old format (lists)
roles.client.tags = [ "backup" ];
roles.server.machines = [ "blob64" ];
# ✅ New format (attribute sets)
roles.client.tags.backup = { };
roles.server.machines.blob64 = { };
```
### Handling Multiple Machines/Tags
When you need to assign multiple machines or tags to a role:
```nix
# ❌ Old format
roles.moon.machines = [ "eva" "eve" ];
# ✅ New format - each machine gets its own attribute
roles.moon.machines.eva = { };
roles.moon.machines.eve = { };
```
---
!!! Warning
@@ -138,8 +246,89 @@ roles.default.machines."test-inventory-machine".settings = {
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
* Module authors should begin exporting service modules under the `clan.modules` attribute of their flake.
## Troubleshooting Common Migration Errors
### Error: "not of type `attribute set of (submodule)`"
This error occurs when using lists instead of attribute sets for tags or machines:
```
error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.
```
**Solution**: Convert lists to attribute sets as shown in the "Important Type Changes" section above.
### Error: "unsupported attribute `module`"
This error indicates the module structure is incorrect:
```
error: Module ':anon-4:anon-1' has an unsupported attribute `module'.
```
**Solution**: Ensure the `module` attribute has exactly two fields: `name` and `input`.
### Error: "attribute 'pkgs' missing"
This suggests the instance configuration is trying to use imports incorrectly:
```
error: attribute 'pkgs' missing
```
**Solution**: Use the `module = { name = "..."; input = "..."; }` format instead of `imports`.
### Removed Features
The following features from the old `services` format are no longer supported in `instances`:
- Top-level `config` attribute (use `roles.<role>.settings` instead)
- Direct module imports (use the `module` declaration instead)
### extraModules Support
The `extraModules` attribute is still supported in the new instances format! The key change is how modules are specified:
**Old format (string paths relative to clan root):**
```nix
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
```
**New format (NixOS modules):**
```nix
# Direct module reference
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
# Or using self
roles.client.extraModules = [ self.nixosModules.borgbackup ];
# Or inline module definition
roles.client.extraModules = [
{ config, ... }: {
# Your module configuration here
}
];
```
The `extraModules` now expects actual **NixOS modules** rather than string paths. This provides better type checking and more flexibility in how modules are specified.
**Alternative: Using @clan/importer**
For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated `@clan/importer` service:
```nix
instances = {
my-importer = {
module.name = "@clan/importer";
module.input = "clan-core";
roles.default.tags.my-tag = { };
roles.default.extraModules = [ self.nixosModules.myModule ];
};
};
```
## Further reference
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
* [ClanServices](../clanServices.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)

35
flake.lock generated
View File

@@ -8,19 +8,16 @@
"nixpkgs": [
"nixpkgs"
],
"systems": [
"systems"
],
"treefmt-nix": [
"treefmt-nix"
]
},
"locked": {
"lastModified": 1751846468,
"narHash": "sha256-h0mpWZIOIAKj4fmLNyI2HDG+c0YOkbYmyJXSj/bQ9s0=",
"rev": "a2166c13b0cb3febdaf36391cd2019aa2ccf4366",
"lastModified": 1753067306,
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/a2166c13b0cb3febdaf36391cd2019aa2ccf4366.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
},
"original": {
"type": "tarball",
@@ -34,11 +31,11 @@
]
},
"locked": {
"lastModified": 1752113600,
"narHash": "sha256-7LYDxKxZgBQ8LZUuolAQ8UkIB+jb4A2UmiR+kzY9CLI=",
"lastModified": 1752718651,
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
"owner": "nix-community",
"repo": "disko",
"rev": "79264292b7e3482e5702932949de9cbb69fedf6d",
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
"type": "github"
},
"original": {
@@ -118,10 +115,10 @@
"nixpkgs": {
"locked": {
"lastModified": 315532800,
"narHash": "sha256-mUlYenGbsUFP0A3EhfKJXmUl5+MQGJLhoEop2t3g5p4=",
"rev": "ceb24d94c6feaa4e8737a8e2bd3cf71c3a7eaaa0",
"narHash": "sha256-lUi+sPH7Kuh9uP3PyfgbENcJGReUM8Ffk9GxGBFbSN8=",
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre826033.ceb24d94c6fe/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre827262.be9e214982e2/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
@@ -149,11 +146,11 @@
]
},
"locked": {
"lastModified": 1751606940,
"narHash": "sha256-KrDPXobG7DFKTOteqdSVeL1bMVitDcy7otpVZWDE6MA=",
"lastModified": 1752544651,
"narHash": "sha256-GllP7cmQu7zLZTs9z0J2gIL42IZHa9CBEXwBY9szT0U=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "3633fc4acf03f43b260244d94c71e9e14a2f6e0d",
"rev": "2c8def626f54708a9c38a5861866660395bb3461",
"type": "github"
},
"original": {
@@ -184,11 +181,11 @@
]
},
"locked": {
"lastModified": 1752055615,
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
"lastModified": 1753006367,
"narHash": "sha256-tzbhc4XttkyEhswByk5R38l+ztN9UDbnj0cTcP6Hp9A=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
"rev": "421b56313c65a0815a52b424777f55acf0b56ddf",
"type": "github"
},
"original": {

View File

@@ -30,7 +30,6 @@
inputs = {
flake-parts.follows = "flake-parts";
nixpkgs.follows = "nixpkgs";
systems.follows = "systems";
treefmt-nix.follows = "treefmt-nix";
};
};

View File

@@ -21,7 +21,6 @@ lib.fix (
{
inherit (buildClanLib)
buildClan
clan
;
/**

View File

@@ -78,7 +78,87 @@ in
internal = true;
visible = false;
type = types.deferredModule;
default = { };
default = {
options.networking = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
priority = lib.mkOption {
type = lib.types.int;
default = 1000;
description = ''
priority with which this network should be tried.
higher priority means it gets used earlier in the chain
'';
};
module = lib.mkOption {
# type = lib.types.enum [
# "clan_lib.network.direct"
# "clan_lib.network.tor"
# ];
type = lib.types.str;
default = "clan_lib.network.direct";
description = ''
the technology this network uses to connect to the target
This is used for userspace networking with socks proxies.
'';
};
# should we call this machines? hosts?
peers = lib.mkOption {
# <name>
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
};
SSHOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
host = lib.mkOption {
description = '''';
type = lib.types.attrTag {
plain = lib.mkOption {
type = lib.types.str;
description = ''
a plain value, which can be read directly from the config
'';
};
var = lib.mkOption {
type = lib.types.submodule {
options = {
machine = lib.mkOption {
type = lib.types.str;
example = "jon";
};
generator = lib.mkOption {
type = lib.types.str;
example = "tor-ssh";
};
file = lib.mkOption {
type = lib.types.str;
example = "hostname";
};
};
};
};
};
};
};
}
)
);
};
};
}
);
};
};
description = ''
A module that is used to define the module of flake level exports -

View File

@@ -101,19 +101,15 @@ let
system:
lib.nameValuePair system (
lib.mapAttrs (
name: _:
moduleSystemConstructor.${machineClasses.${name}} {
_: machine:
machine.extendModules {
modules = [
(config.outputs.moduleForMachine.${name} or { })
(lib.modules.importApply ../machineModules/overridePkgs.nix {
pkgs = pkgsFor.${system};
})
];
specialArgs = {
inherit clan-core;
} // specialArgs;
}
) allMachines
) configurations
)
) supportedSystems
);

View File

@@ -5,11 +5,7 @@
clan-core,
...
}:
rec {
buildClan =
# TODO: Once all templates and docs are migrated add: lib.warn "'buildClan' is deprecated. Use 'clan-core.lib.clan' instead"
module: (clan module).config;
{
clan =
{
self ? lib.warn "Argument: 'self' must be set" null, # Reference to the current flake

View File

@@ -48,6 +48,7 @@ in
{
options = {
instances = lib.mkOption {
default = { };
# instances.<instanceName>...
type = types.attrsOf (submoduleWith {
modules = [
@@ -57,6 +58,7 @@ in
};
# instances.<machineName>...
machines = lib.mkOption {
default = { };
type = types.attrsOf (submoduleWith {
modules = [
config.exportsModule

View File

@@ -26,6 +26,7 @@ in
++ lib.optionals (_class == "nixos") [
./secret/password-store.nix
];
options.clan.core.vars = lib.mkOption {
description = ''
Generated Variables
@@ -36,7 +37,14 @@ in
- generate secrets like private keys automatically when they are needed
- output multiple values like private and public keys simultaneously
'';
type = submodule { imports = [ ./interface.nix ]; };
type = submodule {
imports = [
./interface.nix
{
settings.dependenciesType = lib.types.listOf lib.types.str;
}
];
};
};
config = {

View File

@@ -10,6 +10,9 @@ let
hashString
toJSON
;
inherit (lib)
mkOption
;
inherit (lib.types)
attrsOf
bool
@@ -22,20 +25,29 @@ let
package
path
str
strMatching
submoduleWith
;
# the original types.submodule has strange behavior
submodule =
module:
submoduleWith {
specialArgs.pkgs = pkgs;
modules = [ module ];
};
submoduleWithPkgs =
module:
submoduleWith {
modules = [
module
{ config._module.args.pkgs = pkgs; }
];
};
in
{
options = {
settings = import ./settings-opts.nix { inherit lib; };
generators = lib.mkOption {
generators = mkOption {
description = ''
A set of generators that can be used to generate files.
Generators are scripts that produce files based on the values of other generators and user input.
@@ -43,11 +55,11 @@ in
'';
default = { };
type = attrsOf (
submodule (generator: {
submoduleWithPkgs (generator: {
imports = [ ./generator.nix ];
options = {
name = lib.mkOption {
type = lib.types.str;
name = mkOption {
type = str;
description = ''
The name of the generator.
This name will be used to refer to the generator in other generators.
@@ -56,8 +68,7 @@ in
default = generator.config._module.args.name;
defaultText = "Name of the generator";
};
dependencies = lib.mkOption {
dependencies = mkOption {
description = ''
A list of other generators that this generator depends on.
The output values of these generators will be available to the generator script as files.
@@ -66,10 +77,10 @@ in
**A file `file1` of a generator named `dep1` will be available via `$in/dep1/file1`**
'';
type = listOf str;
type = config.settings.dependenciesType;
default = [ ];
};
migrateFact = lib.mkOption {
migrateFact = mkOption {
description = ''
The fact service name to import the files from.
@@ -79,7 +90,7 @@ in
example = "my_service";
default = null;
};
validation = lib.mkOption {
validation = mkOption {
description = ''
A set of values that invalidate the generated values.
If any of these values change, the generated values will be re-generated.
@@ -106,7 +117,7 @@ in
]);
};
# the validationHash is the validation interface to the outside world
validationHash = lib.mkOption {
validationHash = mkOption {
internal = true;
description = ''
A hash of the invalidation data.
@@ -122,7 +133,7 @@ in
hashString "sha256" (toJSON generator.config.validation);
defaultText = "Hash of the invalidation data";
};
files = lib.mkOption {
files = mkOption {
description = ''
A set of files to generate.
The generator 'script' is expected to produce exactly these files under $out.
@@ -152,8 +163,8 @@ in
];
options =
{
name = lib.mkOption {
type = lib.types.str;
name = mkOption {
type = str;
description = ''
name of the public fact
'';
@@ -161,8 +172,8 @@ in
default = file.config._module.args.name;
defaultText = "Name of the file";
};
generatorName = lib.mkOption {
type = lib.types.str;
generatorName = mkOption {
type = str;
description = ''
name of the generator
'';
@@ -170,8 +181,8 @@ in
default = generator.config._module.args.name;
defaultText = "Name of the generator that generates this file";
};
share = lib.mkOption {
type = lib.types.bool;
share = mkOption {
type = bool;
description = ''
Whether the generated vars should be shared between machines.
Shared vars are only generated once, when the first machine using it is deployed.
@@ -182,7 +193,7 @@ in
default = generator.config.share;
defaultText = "Mirror of the share flag of the generator";
};
deploy = lib.mkOption {
deploy = mkOption {
description = ''
Whether the file should be deployed to the target machine.
@@ -191,14 +202,14 @@ in
type = bool;
default = true;
};
secret = lib.mkOption {
secret = mkOption {
description = ''
Whether the file should be treated as a secret.
'';
type = bool;
default = true;
};
flakePath = lib.mkOption {
flakePath = mkOption {
description = ''
The path to the file containing the content of the generated value.
This will be set automatically
@@ -206,7 +217,7 @@ in
type = nullOr path;
default = null;
};
path = lib.mkOption {
path = mkOption {
description = ''
The path to the file containing the content of the generated value.
This will be set automatically
@@ -223,7 +234,7 @@ in
path = file.config.flakePath;
};
};
neededFor = lib.mkOption {
neededFor = mkOption {
description = ''
This option determines when the secret will be decrypted and deployed to the target machine.
@@ -233,7 +244,7 @@ in
By setting this to `user`, the secret will be deployed prior to users and groups are created, allowing
users' passwords to be managed by vars. The secret will be stored in `/run/secrets-for-users` and `owner` and `group` must be `root`.
'';
type = lib.types.enum [
type = enum [
"partitioning"
"activation"
"users"
@@ -241,22 +252,22 @@ in
];
default = "services";
};
owner = lib.mkOption {
owner = mkOption {
description = "The user name or id that will own the file.";
default = "root";
};
group = lib.mkOption {
group = mkOption {
description = "The group name or id that will own the file.";
default = if _class == "darwin" then "wheel" else "root";
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
};
mode = lib.mkOption {
type = lib.types.strMatching "^[0-7]{4}$";
mode = mkOption {
type = strMatching "^[0-7]{4}$";
description = "The unix file mode of the file. Must be a 4-digit octal number.";
default = "0400";
};
value =
lib.mkOption {
mkOption {
description = ''
The content of the generated value.
Only available if the file is not secret.
@@ -269,7 +280,7 @@ in
};
}
// (lib.optionalAttrs (_class == "nixos") {
restartUnits = lib.mkOption {
restartUnits = mkOption {
description = ''
A list of systemd units that should be restarted after the file is deployed.
This is useful for services that need to reload their configuration after the file is updated.
@@ -283,7 +294,7 @@ in
})
);
};
prompts = lib.mkOption {
prompts = mkOption {
description = ''
A set of prompts to ask the user for values.
Prompts are available to the generator script as files.
@@ -293,7 +304,7 @@ in
type = attrsOf (
submodule (prompt: {
options = {
name = lib.mkOption {
name = mkOption {
description = ''
The name of the prompt.
This name will be used to refer to the prompt in the generator script.
@@ -302,7 +313,7 @@ in
default = prompt.config._module.args.name;
defaultText = "Name of the prompt";
};
persist = lib.mkOption {
persist = mkOption {
description = ''
Whether the prompted value should be stored in a file with the same name as the prompt.
@@ -317,7 +328,7 @@ in
type = bool;
default = false;
};
description = lib.mkOption {
description = mkOption {
description = ''
The description of the prompted value
'';
@@ -326,7 +337,7 @@ in
default = prompt.config._module.args.name;
defaultText = "Name of the prompt";
};
type = lib.mkOption {
type = mkOption {
description = ''
The input type of the prompt.
The following types are available:
@@ -347,7 +358,7 @@ in
})
);
};
runtimeInputs = lib.mkOption {
runtimeInputs = mkOption {
description = ''
A list of packages that the generator script requires.
These packages will be available in the PATH when the script is run.
@@ -355,7 +366,7 @@ in
type = listOf package;
default = [ ];
};
script = lib.mkOption {
script = mkOption {
description = ''
The script to run to generate the files.
The script will be run with the following environment variables:
@@ -369,17 +380,17 @@ in
type = either str path;
default = "";
};
finalScript = lib.mkOption {
finalScript = mkOption {
description = ''
The final generator script, wrapped, so:
- all required programs are in PATH
- sandbox is set up correctly
'';
type = lib.types.path;
type = path;
readOnly = true;
internal = true;
};
share = lib.mkOption {
share = mkOption {
description = ''
Whether the generated vars should be shared between machines.
Shared vars are only generated once, when the first machine using it is deployed.

View File

@@ -65,4 +65,14 @@
Set it to pkgs.pass for GPG or pkgs.passage for age encryption.
'';
};
dependenciesType = lib.mkOption {
type = lib.types.raw;
description = ''
The type of the `dependencies` option.
'';
internal = true;
readOnly = true;
visible = false;
};
}

View File

@@ -29,10 +29,7 @@ def _get_lib_names() -> list[str]:
msg = f"Unsupported architecture: {machine}"
raise RuntimeError(msg)
if system == "darwin":
if machine == "arm64":
return ["libwebview.dylib"]
msg = "Not supported"
raise RuntimeError(msg)
return ["libwebview.dylib"]
# linux
return ["libwebview.so"]

View File

@@ -21,6 +21,12 @@ buildNpmPackage (_finalAttrs: {
mkdir -p api
cp -r ${clan-ts-api}/* api
cp -r ${fonts} ".fonts"
# only needed for the next couple weeks to make sure this file doesn't make it back into the git history
if [[ -f "${./ui}/src/routes/Onboarding/background.jpg" ]]; then
echo "background.jpg found, exiting"
exit 1
fi
'';
# todo figure out why this fails only inside of Nix

View File

@@ -3,7 +3,7 @@ import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
const config: StorybookConfig = {
framework: "@kachurun/storybook-solid-vite",
stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.tsx"],
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
addons: [
"@storybook/addon-links",
"@storybook/addon-docs",

View File

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="223" height="89" fill="currentColor">
<g clip-path="url(#a)">
<path d="M55.503 18.696h10.104a1.946 1.946 0 0 0 1.943-1.948v-7.79c0-1.075-.87-1.947-1.943-1.947h-3.186a1.863 1.863 0 0 1-1.866-1.87V1.947C60.555.872 59.685 0 58.612 0h-27.98a1.946 1.946 0 0 0-1.944 1.947v3.194c0 1.036-.832 1.87-1.865 1.87h-3.187a1.946 1.946 0 0 0-1.943 1.947v3.194c0 1.036-.832 1.87-1.866 1.87h-3.186a1.946 1.946 0 0 0-1.943 1.947s-.467 1.153-.467 23.253c0 19.763.467 21.913.467 21.913 0 1.075.87 1.948 1.943 1.948h3.186c1.034 0 1.866.833 1.866 1.87v3.271c0 1.036.831 1.87 1.865 1.87h3.265c1.033 0 1.865.833 1.865 1.87v3.193c0 1.075.87 1.948 1.943 1.948h27.981a1.946 1.946 0 0 0 1.943-1.948v-3.194c0-1.036.832-1.87 1.866-1.87h5.145a1.946 1.946 0 0 0 1.943-1.947v-9.285c0-1.075-.87-1.948-1.943-1.948H55.503a1.946 1.946 0 0 0-1.943 1.948v4.69c0 1.035-.832 1.869-1.866 1.869H37.55a1.863 1.863 0 0 1-1.866-1.87v-4.752c0-1.075-.87-1.947-1.943-1.947H29c-1.034 0-1.609.148-1.865-1.87-.078-.646-.125-1.44-.18-2.508-.147-2.68-.287-5.5-.287-13.539 0-11.24.288-16.81.466-18.369.18-1.558.832-1.87 1.866-1.87h4.741a1.946 1.946 0 0 0 1.943-1.947v-3.193c0-1.037.832-1.87 1.866-1.87h14.145c1.034 0 1.866.833 1.866 1.87v3.193c0 1.075.87 1.948 1.943 1.948M20.247 74.822h-2.293a.814.814 0 0 1-.808-.81v-2.298c0-.896-.723-1.62-1.617-1.62H9.327c-.894 0-1.617.724-1.617 1.62v2.298c0 .444-.365.81-.808.81H4.609c-.894 0-1.617.725-1.617 1.62v6.217c0 .896.723 1.62 1.617 1.62h2.293c.443 0 .808.366.808.81v2.299c0 .895.723 1.62 1.617 1.62h6.202c.894 0 1.617-.725 1.617-1.62v-2.299c0-.444.365-.81.808-.81h2.293c.894 0 1.617-.724 1.617-1.62v-6.216c0-.896-.723-1.62-1.617-1.62M221.135 35.04h-1.71a1.863 1.863 0 0 1-1.866-1.87v-3.272c0-1.036-.831-1.87-1.865-1.87h-3.265a1.863 1.863 0 0 1-1.865-1.87v-3.271c0-1.036-.832-1.87-1.865-1.87h-20.971a1.863 1.863 0 0 0-1.865 1.87v3.965c0 .514-.42.935-.933.935h-3.559c-.513 0-.84-.32-.933-.935l-.622-3.918c-.148-1.099-.676-1.777-1.788-1.777l-3.653-.14h-2.052a3.736 3.736 0 0 0-3.73 3.74V61.68a3.736 3.736 0 0 1-3.731 3.739h-8.394a1.863 1.863 0 0 1-1.866-1.87V36.714c0-11.825-7.461-18.813-22.556-18.813-13.718 0-20.325 5.04-21.203 14.443-.109 1.153.552 1.815 1.702 1.815l7.757.569c1.143.1 1.594-.554 1.811-1.652.77-3.74 4.174-5.827 9.933-5.827 7.081 0 10.042 3.358 10.042 9.076v3.014c0 1.036-.831 1.87-1.865 1.87l-.342-.024h-9.715c-15.421 0-22.984 5.983-22.984 17.956 0 3.802.778 7.058 2.254 9.738h-.59c-1.765-1.27-2.457-2.236-3.055-2.93-.256-.295-.653-.537-1.345-.537h-1.717l-5.993.008h-3.264a3.736 3.736 0 0 1-3.731-3.74V1.769C89.74.654 89.072 0 87.969 0H79.55c-1.034 0-1.865.732-1.865 1.768l-.024 54.304v13.554c0 4.13 3.343 7.479 7.462 7.479h50.84c8.448-.429 8.604-3.42 9.436-4.542.645 3.56 1.865 4.347 4.71 4.518 8.137.117 18.343.032 18.49.024h4.975c4.119 0 6.684-3.35 6.684-7.479l.777-27.264c0-1.036.832-1.87 1.866-1.87h2.021a1.56 1.56 0 0 0 1.554-1.558v-3.583c0-1.036.832-1.87 1.866-1.87h11.868a3.37 3.37 0 0 1 3.366 3.373v3.249c0 1.075.87 1.947 1.943 1.947h4.119c.513 0 .933.42.933.935v32.25c0 1.036.831 1.87 1.865 1.87h6.84a3.736 3.736 0 0 0 3.731-3.74V36.91c0-1.036-.832-1.87-1.866-1.87zM142.64 54.225c0 8.927-6.132 14.715-15.335 14.715-6.606 0-9.793-2.953-9.793-8.748 0-6.442 3.832-9.636 11.62-9.636h13.508v3.669"/>
</g>
<defs>
<clipPath id="a">
<path d="M0 0h223v89H0z"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -22,4 +22,4 @@
<rect x="24" y="24" width="6" height="6"/>
<rect x="29" y="24" width="6" height="6"/>
<rect x="6" y="30" width="6" height="6"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 26" fill="none">
<path d="M15.2735 5.89115H18.2129C18.5249 5.89115 18.7781 5.63627 18.7781 5.32222V3.04651C18.7781 2.73246 18.5249 2.47758 18.2129 2.47758H17.2858C16.9851 2.47758 16.7432 2.23408 16.7432 1.93141V0.998371C16.7432 0.684323 16.4899 0.429443 16.1779 0.429443H8.03798C7.72595 0.429443 7.47271 0.684323 7.47271 0.998371V1.93141C7.47271 2.23408 7.23077 2.47758 6.93005 2.47758H6.003C5.69097 2.47758 5.43773 2.73246 5.43773 3.04651V3.97955C5.43773 4.28222 5.1958 4.52572 4.89507 4.52572H3.96803C3.656 4.52572 3.40276 4.7806 3.40276 5.09465C3.40276 5.09465 3.26709 5.43146 3.26709 11.4582C3.26709 17.2317 3.40276 17.8598 3.40276 17.8598C3.40276 18.1739 3.656 18.4287 3.96803 18.4287H4.89507C5.1958 18.4287 5.43773 18.6722 5.43773 18.9749V19.9307C5.43773 20.2334 5.67967 20.4769 5.98039 20.4769H6.93005C7.23077 20.4769 7.47271 20.7204 7.47271 21.023V21.9561C7.47271 22.2701 7.72595 22.525 8.03798 22.525H16.1779C16.4899 22.525 16.7432 22.2701 16.7432 21.9561V21.023C16.7432 20.7204 16.9851 20.4769 17.2858 20.4769H18.7827C19.0947 20.4769 19.3479 20.222 19.3479 19.9079V17.1953C19.3479 16.8812 19.0947 16.6264 18.7827 16.6264H15.2735C14.9614 16.6264 14.7082 16.8812 14.7082 17.1953V18.5653C14.7082 18.8679 14.4662 19.1114 14.1655 19.1114H10.0503C9.74962 19.1114 9.50768 18.8679 9.50768 18.5653V17.1771C9.50768 16.863 9.25444 16.6082 8.94241 16.6082H7.56315C7.26243 16.6082 7.09511 16.6514 7.02049 16.062C6.99788 15.8731 6.98431 15.641 6.96849 15.3292C6.92552 14.5464 6.88483 13.7226 6.88483 11.374C6.88483 8.5196 6.96849 6.89246 7.02049 6.43732C7.0725 5.98218 7.26243 5.89115 7.56315 5.89115H8.94241C9.25444 5.89115 9.50768 5.63627 9.50768 5.32222V4.38918C9.50768 4.08651 9.74962 3.84301 10.0503 3.84301H14.1655C14.4662 3.84301 14.7082 4.08651 14.7082 4.38918V5.32222C14.7082 5.63627 14.9614 5.89115 15.2735 5.89115Z" fill="currentColor"/>
<path d="M5.01717 21.8582H4.35015C4.22126 21.8582 4.11499 21.7513 4.11499 21.6216V20.9502C4.11499 20.6885 3.90471 20.4769 3.64469 20.4769H1.84034C1.58032 20.4769 1.37004 20.6885 1.37004 20.9502V21.6216C1.37004 21.7513 1.26377 21.8582 1.13488 21.8582H0.467864C0.207839 21.8582 -0.00244141 22.0699 -0.00244141 22.3316V24.1476C-0.00244141 24.4093 0.207839 24.6209 0.467864 24.6209H1.13488C1.26377 24.6209 1.37004 24.7279 1.37004 24.8576V25.5289C1.37004 25.7907 1.58032 26.0023 1.84034 26.0023H3.64469C3.90471 26.0023 4.11499 25.7907 4.11499 25.5289V24.8576C4.11499 24.7279 4.22126 24.6209 4.35015 24.6209H5.01717C5.27719 24.6209 5.48747 24.4093 5.48747 24.1476V22.3316C5.48747 22.0699 5.27719 21.8582 5.01717 21.8582Z" fill="currentColor"/>
<path d="M63.4576 10.2361H62.9601C62.6594 10.2361 62.4175 9.99265 62.4175 9.68998V8.73418C62.4175 8.43151 62.1755 8.18801 61.8748 8.18801H60.9252C60.6244 8.18801 60.3825 7.94451 60.3825 7.64184V6.68604C60.3825 6.38337 60.1406 6.13987 59.8398 6.13987H53.7394C53.4387 6.13987 53.1968 6.38337 53.1968 6.68604V7.84438C53.1968 7.99457 53.0747 8.11746 52.9254 8.11746H51.8899C51.7406 8.11746 51.6457 8.02416 51.6185 7.84438L51.4376 6.69969C51.3947 6.37882 51.2409 6.18083 50.9176 6.18083L49.8549 6.13987H49.258C48.6588 6.13987 48.1726 6.62915 48.1726 7.23221V18.0191C48.1726 18.6221 47.6865 19.1114 47.0873 19.1114H44.6453C44.3446 19.1114 44.1027 18.8679 44.1027 18.5653V10.7254C44.1027 7.2709 41.932 5.22958 37.541 5.22958C33.5502 5.22958 31.6283 6.70197 31.3728 9.44875C31.3411 9.78556 31.5333 9.97899 31.868 9.97899L34.1245 10.1451C34.4569 10.1747 34.588 9.98354 34.6514 9.66267C34.8752 8.57033 35.8656 7.96044 37.541 7.96044C39.6009 7.96044 40.4623 8.94127 40.4623 10.6116V11.4923C40.4623 11.795 40.2204 12.0385 39.9197 12.0385L39.8202 12.0317H36.9938C32.5078 12.0317 30.3078 13.7794 30.3078 17.2772C30.3078 18.3877 30.5339 19.339 30.9635 20.1218H30.7917C30.2784 19.7509 30.0772 19.4687 29.9031 19.2662C29.8285 19.1797 29.7131 19.1091 29.5119 19.1091H29.0122L27.2689 19.1114H26.3193C25.7201 19.1114 25.2339 18.6221 25.2339 18.0191V0.516586C25.2339 0.19116 25.0395 0 24.7184 0H22.2697C21.9689 0 21.727 0.213917 21.727 0.516586L21.7202 16.3806V20.3403C21.7202 21.5464 22.6925 22.525 23.8909 22.525H38.6806C41.1384 22.3998 41.1836 21.5259 41.4256 21.1982C41.6132 22.2382 41.9682 22.4681 42.7958 22.5182C45.1631 22.5523 48.1319 22.5273 48.1749 22.525H49.622C50.8204 22.525 51.5665 21.5464 51.5665 20.3403L51.7926 12.3753C51.7926 12.0726 52.0346 11.8291 52.3353 11.8291H52.9232C53.1719 11.8291 53.3754 11.6243 53.3754 11.374V10.3272C53.3754 10.0245 53.6173 9.781 53.9181 9.781H57.3707C57.9111 9.781 58.3498 10.2225 58.3498 10.7664V11.7154C58.3498 12.0294 58.603 12.2843 58.915 12.2843H60.1134C60.2626 12.2843 60.3848 12.4072 60.3848 12.5574V21.9788C60.3848 22.2815 60.6267 22.525 60.9274 22.525H62.9172C63.5164 22.525 64.0025 22.0357 64.0025 21.4326V10.7823C64.0025 10.4796 63.7606 10.2361 63.4598 10.2361H63.4576ZM40.6229 15.8412C40.6229 18.4492 38.8389 20.14 36.1618 20.14C34.2398 20.14 33.3128 19.2775 33.3128 17.5844C33.3128 15.7024 34.4275 14.7694 36.6931 14.7694H40.6229V15.8389V15.8412Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,43 @@
<svg viewBox="0 0 200 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M191.879 19.297C191.736 19.297 191.602 19.2434 191.477 19.1362C191.37 19.029 191.316 18.9039 191.316 18.761V6.16432C191.316 6.02138 191.37 5.8963 191.477 5.7891C191.602 5.68189 191.736 5.62829 191.879 5.62829H193.246C193.568 5.62829 193.764 5.80697 193.836 6.16432C193.907 6.48594 193.979 6.72715 194.05 6.88796C194.139 7.0309 194.273 7.10237 194.452 7.10237C194.559 7.10237 194.676 7.06663 194.801 6.99516C194.926 6.90582 195.051 6.80755 195.176 6.70035C195.569 6.36086 195.971 6.10178 196.382 5.92311C196.793 5.72656 197.365 5.62829 198.097 5.62829H199.464C199.607 5.62829 199.732 5.68189 199.839 5.7891C199.946 5.8963 200 6.02138 200 6.16432V8.28163C200 8.42457 199.946 8.54964 199.839 8.65685C199.732 8.76405 199.607 8.81766 199.464 8.81766H197.936C196.9 8.81766 196.042 9.12141 195.363 9.7289C194.702 10.3364 194.372 11.2745 194.372 12.543V18.761C194.372 18.9039 194.318 19.029 194.211 19.1362C194.104 19.2434 193.979 19.297 193.836 19.297H191.879Z"
fill="currentColor"/>
<path d="M182.709 19.7526C180.583 19.7526 178.93 19.0915 177.751 17.7693C176.589 16.4471 176.009 14.6782 176.009 12.4626C176.009 11.0154 176.268 9.74677 176.786 8.65685C177.304 7.54906 178.064 6.69141 179.064 6.08391C180.065 5.47642 181.253 5.17267 182.629 5.17267C184.648 5.17267 186.238 5.7623 187.399 6.94156C188.579 8.12082 189.168 9.74677 189.168 11.8194V12.7843C189.168 13.0165 189.106 13.1952 188.981 13.3203C188.856 13.4454 188.686 13.5079 188.471 13.5079H179.573C179.395 13.5079 179.305 13.6062 179.305 13.8027C179.341 14.839 179.681 15.6609 180.324 16.2684C180.967 16.8581 181.762 17.1529 182.709 17.1529C184.031 17.1529 184.978 16.6526 185.55 15.652C185.747 15.3304 185.97 15.1696 186.22 15.1696H188.284C188.445 15.1696 188.579 15.2321 188.686 15.3572C188.793 15.4644 188.82 15.6163 188.766 15.8128C188.427 16.9921 187.73 17.948 186.676 18.6806C185.622 19.3953 184.299 19.7526 182.709 19.7526ZM179.279 10.801C179.279 10.9796 179.377 11.069 179.573 11.069H185.791C185.952 11.069 186.032 10.9796 186.032 10.801C186.032 9.92545 185.72 9.19288 185.094 8.60325C184.487 7.99575 183.665 7.692 182.629 7.692C181.664 7.692 180.869 7.99575 180.243 8.60325C179.636 9.19288 179.314 9.92545 179.279 10.801Z"
fill="currentColor"/>
<path d="M166.551 19.7526C165.621 19.7526 164.773 19.6008 164.004 19.297C163.236 18.9754 162.584 18.5108 162.048 17.9033C161.44 17.2601 160.976 16.4739 160.654 15.5448C160.333 14.6157 160.172 13.5883 160.172 12.4627C160.172 11.337 160.333 10.3096 160.654 9.38049C160.976 8.45137 161.44 7.6652 162.048 7.02197C162.584 6.43234 163.236 5.97671 164.004 5.6551C164.773 5.33348 165.621 5.17267 166.551 5.17267C167.247 5.17267 167.801 5.24414 168.212 5.38708C168.641 5.51215 169.079 5.69083 169.526 5.92311C169.561 5.94098 169.66 5.99458 169.82 6.08392C169.981 6.17326 170.133 6.21792 170.276 6.21792C170.598 6.21792 170.758 6.03032 170.758 5.6551V0.536027C170.758 0.393087 170.812 0.268014 170.919 0.160808C171.026 0.0536026 171.16 0 171.321 0H173.305C173.447 0 173.573 0.0536026 173.68 0.160808C173.787 0.268014 173.841 0.393087 173.841 0.536027V18.761C173.841 18.9039 173.787 19.029 173.68 19.1362C173.573 19.2434 173.447 19.297 173.305 19.297H171.911C171.661 19.297 171.446 19.1541 171.268 18.8682C171.16 18.7252 171.053 18.618 170.946 18.5466C170.857 18.4751 170.732 18.4394 170.571 18.4394C170.356 18.4394 170.062 18.5644 169.686 18.8146C169.24 19.1005 168.793 19.3238 168.346 19.4846C167.9 19.6633 167.301 19.7526 166.551 19.7526ZM163.308 12.4627C163.308 13.6955 163.62 14.6961 164.246 15.4644C164.567 15.8575 164.969 16.1702 165.452 16.4025C165.934 16.6169 166.488 16.7241 167.113 16.7241C168.328 16.7241 169.284 16.3042 169.981 15.4644C170.589 14.6961 170.892 13.6955 170.892 12.4627C170.892 11.2298 170.589 10.2381 169.981 9.4877C169.302 8.63005 168.346 8.20123 167.113 8.20123C166.488 8.20123 165.934 8.31737 165.452 8.54965C164.969 8.76406 164.567 9.07674 164.246 9.4877C163.62 10.2203 163.308 11.2119 163.308 12.4627Z"
fill="currentColor"/>
<path d="M155.415 19.297C155.272 19.297 155.147 19.2434 155.04 19.1362C154.933 19.029 154.879 18.9039 154.879 18.761V0.536027C154.879 0.393087 154.933 0.268014 155.04 0.160808C155.147 0.0536026 155.272 0 155.415 0H157.399C157.542 0 157.667 0.0536026 157.774 0.160808C157.881 0.268014 157.935 0.393087 157.935 0.536027V18.761C157.935 18.9039 157.881 19.029 157.774 19.1362C157.667 19.2434 157.542 19.297 157.399 19.297H155.415Z"
fill="currentColor"/>
<path d="M149.107 19.297C148.946 19.297 148.812 19.2434 148.705 19.1362C148.615 19.029 148.571 18.9039 148.571 18.761V6.16432C148.571 6.02138 148.615 5.89631 148.705 5.7891C148.812 5.68189 148.946 5.62829 149.107 5.62829H151.09C151.251 5.62829 151.376 5.68189 151.465 5.7891C151.573 5.87844 151.626 6.00351 151.626 6.16432V18.761C151.626 18.9218 151.573 19.0558 151.465 19.163C151.376 19.2523 151.251 19.297 151.09 19.297H149.107ZM148.33 0.696836C148.33 0.58963 148.365 0.491358 148.437 0.40202C148.526 0.312682 148.633 0.268013 148.758 0.268013H151.492C151.617 0.268013 151.715 0.312682 151.787 0.40202C151.876 0.491358 151.921 0.58963 151.921 0.696836V3.18937C151.921 3.31444 151.876 3.42164 151.787 3.51098C151.715 3.58245 151.617 3.61819 151.492 3.61819H148.758C148.633 3.61819 148.526 3.58245 148.437 3.51098C148.365 3.42164 148.33 3.31444 148.33 3.18937V0.696836Z"
fill="currentColor"/>
<path d="M105.963 19.7526C105.213 19.7526 104.614 19.6633 104.168 19.4846C103.721 19.3238 103.274 19.1005 102.828 18.8146C102.452 18.5644 102.158 18.4394 101.943 18.4394C101.782 18.4394 101.648 18.4751 101.541 18.5466C101.452 18.618 101.353 18.7252 101.246 18.8682C101.068 19.1541 100.853 19.297 100.603 19.297H99.2094C99.0664 19.297 98.9414 19.2434 98.8342 19.1362C98.7269 19.029 98.6733 18.9039 98.6733 18.761V0.536027C98.6733 0.393087 98.7269 0.268014 98.8342 0.160808C98.9414 0.0536026 99.0664 0 99.2094 0H101.193C101.336 0 101.461 0.0536026 101.568 0.160808C101.675 0.268014 101.729 0.393087 101.729 0.536027V5.6551C101.729 6.03032 101.898 6.21792 102.238 6.21792C102.381 6.21792 102.533 6.17326 102.694 6.08392C102.854 5.99458 102.953 5.94098 102.988 5.92311C103.435 5.69083 103.864 5.51215 104.275 5.38708C104.704 5.24414 105.266 5.17267 105.963 5.17267C106.892 5.17267 107.741 5.33348 108.509 5.6551C109.278 5.97671 109.93 6.43234 110.466 7.02197C111.073 7.6652 111.538 8.45137 111.86 9.38049C112.181 10.3096 112.342 11.337 112.342 12.4627C112.342 13.5883 112.181 14.6157 111.86 15.5448C111.538 16.4739 111.073 17.2601 110.466 17.9033C109.93 18.5108 109.278 18.9754 108.509 19.297C107.741 19.6008 106.892 19.7526 105.963 19.7526ZM101.621 12.4627C101.621 13.6955 101.925 14.6961 102.533 15.4644C103.23 16.3042 104.185 16.7241 105.4 16.7241C106.026 16.7241 106.58 16.6169 107.062 16.4025C107.545 16.1702 107.947 15.8575 108.268 15.4644C108.894 14.6961 109.206 13.6955 109.206 12.4627C109.206 11.2119 108.894 10.2203 108.268 9.4877C107.947 9.07674 107.545 8.76406 107.062 8.54965C106.58 8.31737 106.026 8.20123 105.4 8.20123C104.168 8.20123 103.212 8.63005 102.533 9.4877C101.925 10.2381 101.621 11.2298 101.621 12.4627Z"
fill="currentColor"/>
<path d="M93.5555 19.297C92.1619 19.297 91.0898 18.9039 90.3394 18.1177C89.6068 17.3316 89.2405 16.2863 89.2405 14.982V8.81765C89.2405 8.63897 89.1512 8.54964 88.9725 8.54964H86.2924C86.1494 8.54964 86.0244 8.49603 85.9172 8.38883C85.81 8.28162 85.7563 8.15655 85.7563 8.01361V6.16431C85.7563 6.02137 85.81 5.8963 85.9172 5.78909C86.0244 5.68189 86.1494 5.62828 86.2924 5.62828H88.9725C89.1512 5.62828 89.2405 5.53895 89.2405 5.36027V1.47407C89.2405 1.33113 89.2941 1.20605 89.4013 1.09885C89.5085 0.991643 89.6336 0.93804 89.7766 0.93804H91.7598C91.9028 0.93804 92.0279 0.991643 92.1351 1.09885C92.2423 1.20605 92.2959 1.33113 92.2959 1.47407V5.36027C92.2959 5.53895 92.3852 5.62828 92.5639 5.62828H96.2625C96.4054 5.62828 96.5305 5.68189 96.6377 5.78909C96.7449 5.8963 96.7985 6.02137 96.7985 6.16431V8.01361C96.7985 8.15655 96.7449 8.28162 96.6377 8.38883C96.5305 8.49603 96.4054 8.54964 96.2625 8.54964H92.5639C92.3852 8.54964 92.2959 8.63897 92.2959 8.81765V14.5531C92.2959 15.107 92.4477 15.5269 92.7515 15.8128C93.0552 16.0987 93.4751 16.2416 94.0112 16.2416H96.2625C96.4054 16.2416 96.5305 16.2952 96.6377 16.4024C96.7449 16.5096 96.7985 16.6347 96.7985 16.7777V18.761C96.7985 18.9039 96.7449 19.029 96.6377 19.1362C96.5305 19.2434 96.4054 19.297 96.2625 19.297H93.5555Z"
fill="currentColor"/>
<path d="M78.7594 19.7526C76.6332 19.7526 74.9804 19.0915 73.8012 17.7693C72.6398 16.4471 72.0591 14.6782 72.0591 12.4626C72.0591 11.0154 72.3182 9.74677 72.8363 8.65685C73.3545 7.54906 74.1139 6.69141 75.1144 6.08391C76.115 5.47642 77.3032 5.17267 78.679 5.17267C80.698 5.17267 82.2883 5.7623 83.4496 6.94156C84.6289 8.12082 85.2185 9.74677 85.2185 11.8194V12.7843C85.2185 13.0165 85.156 13.1952 85.0309 13.3203C84.9059 13.4454 84.7361 13.5079 84.5217 13.5079H75.6237C75.445 13.5079 75.3556 13.6062 75.3556 13.8027C75.3914 14.839 75.7309 15.6609 76.3741 16.2684C77.0173 16.8581 77.8124 17.1529 78.7594 17.1529C80.0816 17.1529 81.0286 16.6526 81.6004 15.652C81.7969 15.3304 82.0202 15.1696 82.2704 15.1696H84.3341C84.4949 15.1696 84.6289 15.2321 84.7361 15.3572C84.8433 15.4644 84.8701 15.6163 84.8165 15.8128C84.477 16.9921 83.7802 17.948 82.726 18.6806C81.6718 19.3953 80.3496 19.7526 78.7594 19.7526ZM75.3288 10.801C75.3288 10.9796 75.4271 11.069 75.6237 11.069H81.8416C82.0024 11.069 82.0828 10.9796 82.0828 10.801C82.0828 9.92545 81.7701 9.19288 81.1447 8.60325C80.5372 7.99575 79.7153 7.692 78.679 7.692C77.7142 7.692 76.9191 7.99575 76.2937 8.60325C75.6862 9.19288 75.3646 9.92545 75.3288 10.801Z"
fill="currentColor"/>
<path d="M58.246 19.297C58.103 19.297 57.978 19.2434 57.8708 19.1362C57.7636 19.029 57.71 18.9039 57.71 18.761V6.16432C57.71 6.02138 57.7636 5.8963 57.8708 5.7891C57.978 5.68189 58.103 5.62829 58.246 5.62829H59.6665C59.8273 5.62829 59.9523 5.66402 60.0417 5.7355C60.131 5.7891 60.2382 5.88737 60.3633 6.03031C60.4526 6.15538 60.533 6.25366 60.6045 6.32513C60.6938 6.37873 60.81 6.40553 60.9529 6.40553C61.078 6.40553 61.2031 6.3698 61.3281 6.29833C61.4532 6.22685 61.5783 6.14645 61.7034 6.05711C62.0964 5.7891 62.4985 5.57469 62.9094 5.41388C63.3382 5.25307 63.9279 5.17267 64.6783 5.17267C65.7146 5.17267 66.6348 5.39601 67.4388 5.8427C68.2429 6.28939 68.8682 6.94156 69.3149 7.7992C69.7795 8.65685 70.0118 9.68424 70.0118 10.8814V18.761C70.0118 18.9039 69.9582 19.029 69.851 19.1362C69.7438 19.2434 69.6187 19.297 69.4757 19.297H67.4924C67.3495 19.297 67.2244 19.2434 67.1172 19.1362C67.01 19.029 66.9564 18.9039 66.9564 18.761V11.3638C66.9564 10.3632 66.6705 9.58596 66.0988 9.03207C65.5449 8.47817 64.8034 8.20122 63.8743 8.20122C62.9452 8.20122 62.1947 8.47817 61.623 9.03207C61.0512 9.58596 60.7653 10.3632 60.7653 11.3638V18.761C60.7653 18.9039 60.7117 19.029 60.6045 19.1362C60.4973 19.2434 60.3722 19.297 60.2293 19.297H58.246Z"
fill="currentColor"/>
<path d="M53.1744 19.297C52.8528 19.297 52.5669 19.1541 52.3168 18.8682L48.0822 13.8295C48.0107 13.7581 47.9303 13.7223 47.8409 13.7223C47.7695 13.7223 47.7069 13.7491 47.6533 13.8027C47.5997 13.8563 47.5729 13.9189 47.5729 13.9903V18.761C47.5729 18.9039 47.5193 19.029 47.4121 19.1362C47.3049 19.2434 47.1798 19.297 47.0369 19.297H45.0536C44.9107 19.297 44.7856 19.2434 44.6784 19.1362C44.5712 19.029 44.5176 18.9039 44.5176 18.761V0.536027C44.5176 0.393087 44.5712 0.268014 44.6784 0.160808C44.7856 0.0536026 44.9107 0 45.0536 0H47.0369C47.1798 0 47.3049 0.0536026 47.4121 0.160808C47.5193 0.268014 47.5729 0.393087 47.5729 0.536027V10.5061C47.5729 10.5955 47.5997 10.667 47.6533 10.7206C47.7069 10.7742 47.7695 10.801 47.8409 10.801C47.9124 10.801 47.9839 10.7652 48.0554 10.6938L52.424 6.00351C52.6741 5.75337 52.9421 5.62829 53.228 5.62829H55.3989C55.5597 5.62829 55.6848 5.6819 55.7741 5.7891C55.8813 5.89631 55.9349 6.02138 55.9349 6.16432C55.9349 6.32513 55.8813 6.45914 55.7741 6.56634L50.8695 11.8462C50.7623 11.9534 50.7087 12.0606 50.7087 12.1678C50.7087 12.275 50.7623 12.3822 50.8695 12.4895L55.8813 18.359C55.9885 18.484 56.0421 18.618 56.0421 18.761C56.0421 18.9218 55.9885 19.0558 55.8813 19.163C55.792 19.2523 55.6669 19.297 55.5061 19.297H53.1744Z"
fill="currentColor"/>
<path d="M34.9505 19.297C34.8076 19.297 34.6736 19.2434 34.5485 19.1362C34.4413 19.029 34.3877 18.9039 34.3877 18.761V6.16432C34.3877 6.02138 34.4413 5.8963 34.5485 5.7891C34.6736 5.68189 34.8076 5.62829 34.9505 5.62829H36.3174C36.639 5.62829 36.8356 5.80697 36.907 6.16432C36.9785 6.48594 37.05 6.72715 37.1214 6.88796C37.2108 7.0309 37.3448 7.10237 37.5235 7.10237C37.6307 7.10237 37.7468 7.06663 37.8719 6.99516C37.9969 6.90582 38.122 6.80755 38.2471 6.70035C38.6402 6.36086 39.0422 6.10178 39.4531 5.92311C39.8641 5.72656 40.4359 5.62829 41.1684 5.62829H42.5353C42.6782 5.62829 42.8033 5.68189 42.9105 5.7891C43.0177 5.8963 43.0713 6.02138 43.0713 6.16432V8.28163C43.0713 8.42457 43.0177 8.54964 42.9105 8.65685C42.8033 8.76405 42.6782 8.81766 42.5353 8.81766H41.0076C39.9713 8.81766 39.1137 9.12141 38.4347 9.7289C37.7736 10.3364 37.443 11.2745 37.443 12.543V18.761C37.443 18.9039 37.3894 19.029 37.2822 19.1362C37.175 19.2434 37.05 19.297 36.907 19.297H34.9505Z"
fill="currentColor"/>
<path d="M23.8885 19.7526C22.9594 19.7526 22.1107 19.6007 21.3424 19.297C20.5741 18.9754 19.9219 18.5108 19.3859 17.9033C18.7784 17.2601 18.3138 16.4739 17.9922 15.5448C17.6706 14.6157 17.5098 13.5883 17.5098 12.4626C17.5098 11.337 17.6706 10.3096 17.9922 9.38048C18.3138 8.45137 18.7784 7.6652 19.3859 7.02196C19.9219 6.43233 20.5741 5.97671 21.3424 5.65509C22.1107 5.33348 22.9594 5.17267 23.8885 5.17267C24.6389 5.17267 25.2375 5.262 25.6842 5.44068C26.1309 5.60149 26.5775 5.82483 27.0242 6.11072C27.3995 6.36086 27.6943 6.48594 27.9087 6.48594C28.0695 6.48594 28.1946 6.4502 28.2839 6.37873C28.3911 6.30726 28.4983 6.20005 28.6055 6.05711C28.7842 5.77123 28.9986 5.62829 29.2487 5.62829H30.6424C30.7854 5.62829 30.9104 5.68189 31.0176 5.7891C31.1248 5.8963 31.1784 6.02138 31.1784 6.16432V18.761C31.1784 18.9039 31.1248 19.029 31.0176 19.1362C30.9104 19.2434 30.7854 19.297 30.6424 19.297H29.2487C28.9986 19.297 28.7842 19.1541 28.6055 18.8682C28.4983 18.7252 28.3911 18.618 28.2839 18.5466C28.1946 18.4751 28.0695 18.4394 27.9087 18.4394C27.6943 18.4394 27.3995 18.5644 27.0242 18.8146C26.5775 19.1005 26.1309 19.3238 25.6842 19.4846C25.2375 19.6633 24.6389 19.7526 23.8885 19.7526ZM20.6455 12.4626C20.6455 13.6955 20.9582 14.6961 21.5836 15.4644C21.9052 15.8575 22.3072 16.1702 22.7896 16.4024C23.2721 16.6169 23.8259 16.7241 24.4513 16.7241C25.0767 16.7241 25.6306 16.6169 26.113 16.4024C26.5954 16.1702 26.9974 15.8575 27.3191 15.4644C27.9265 14.6961 28.2303 13.6955 28.2303 12.4626C28.2303 11.2298 27.9265 10.2381 27.3191 9.48769C26.9974 9.07674 26.5954 8.76405 26.113 8.54964C25.6306 8.31736 25.0767 8.20122 24.4513 8.20122C23.8259 8.20122 23.2721 8.31736 22.7896 8.54964C22.3072 8.76405 21.9052 9.07674 21.5836 9.48769C20.9582 10.2203 20.6455 11.2119 20.6455 12.4626Z"
fill="currentColor"/>
<path d="M0.536027 19.297C0.393086 19.297 0.268013 19.2434 0.160808 19.1362C0.0536025 19.029 0 18.9039 0 18.761V1.07206C0 0.929116 0.0536025 0.804043 0.160808 0.696837C0.268013 0.589631 0.393086 0.536028 0.536027 0.536028H6.83434C9.47874 0.536028 11.6407 1.349 13.3203 2.97495C14.16 3.79686 14.8033 4.80638 15.25 6.00351C15.6966 7.18277 15.92 8.48711 15.92 9.91652C15.92 11.3459 15.6966 12.6592 15.25 13.8563C14.8033 15.0356 14.16 16.0362 13.3203 16.8581C11.6407 18.484 9.47874 19.297 6.83434 19.297H0.536027ZM3.24296 15.786C3.24296 15.9647 3.3323 16.054 3.51098 16.054H6.83434C8.53176 16.054 9.88076 15.5537 10.8813 14.5532C11.4352 14.0171 11.8551 13.356 12.141 12.5699C12.4269 11.7837 12.5698 10.8992 12.5698 9.91652C12.5698 8.9338 12.4269 8.04935 12.141 7.26318C11.8551 6.477 11.4352 5.8159 10.8813 5.27988C9.84502 4.27929 8.49602 3.779 6.83434 3.779H3.51098C3.3323 3.779 3.24296 3.86833 3.24296 4.04701V15.786Z"
fill="currentColor"/>
<g clip-path="url(#clip0_4498_21233)">
<path d="M140.006 0.211082H142.457C143.062 0.211082 143.553 0.705733 143.553 1.31591V4.01815C143.553 4.62833 143.062 5.12298 142.457 5.12298H140.006C139.401 5.12298 138.911 4.62833 138.911 4.01815V1.31591C138.911 0.705733 139.401 0.211082 140.006 0.211082Z"
fill="currentColor"/>
<path d="M116.73 0.211082H119.181C119.786 0.211082 120.277 0.705733 120.277 1.31591V4.01815C120.277 4.62833 119.786 5.12298 119.181 5.12298H116.73C116.125 5.12298 115.635 4.62833 115.635 4.01815V1.31591C115.635 0.705733 116.125 0.211082 116.73 0.211082Z"
fill="currentColor"/>
<path d="M116.673 11.4481H119.184C119.499 11.4481 119.702 11.6806 119.754 12.0236L119.983 13.6601C120.017 13.8373 120.106 13.9363 120.256 13.9363H121.576C121.89 13.9363 122.089 14.1527 122.146 14.5118L122.374 15.8974C122.411 16.192 122.612 16.3716 122.922 16.4498C122.922 16.4498 124.223 16.8572 129.643 16.8572V20.3121C124.225 20.3121 121.119 19.9024 121.119 19.9024C120.861 19.8633 120.549 19.6446 120.549 19.327L120.32 18.1071C120.32 17.9551 120.197 17.8309 120.046 17.8309H118.49C118.175 17.8309 118.002 17.5754 117.919 17.2554L117.691 16.3278C117.657 16.1345 117.568 16.0516 117.417 16.0516H116.902C116.587 16.0516 116.331 15.7938 116.331 15.4762L116.103 12.0236C116.103 11.7059 116.358 11.4481 116.673 11.4481Z"
fill="currentColor"/>
<path d="M136.912 15.8974L137.14 14.5118C137.197 14.1527 137.395 13.9363 137.71 13.9363H139.03C139.18 13.9363 139.269 13.8373 139.303 13.6601L139.532 12.0236C139.584 11.6806 139.787 11.4481 140.102 11.4481H142.613C142.927 11.4481 143.183 11.7059 143.183 12.0236L142.955 15.4762C142.955 15.7938 142.699 16.0516 142.384 16.0516H141.869C141.718 16.0516 141.629 16.1345 141.595 16.3278L141.366 17.2554C141.284 17.5754 141.111 17.8309 140.796 17.8309H139.239C139.089 17.8309 138.966 17.9551 138.966 18.1071L138.737 19.327C138.737 19.6446 138.425 19.8633 138.167 19.9024C138.167 19.9024 135.061 20.3121 129.643 20.3121V16.8572C135.063 16.8572 136.364 16.4498 136.364 16.4498C136.674 16.3716 136.875 16.192 136.912 15.8974Z"
fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_4498_21233">
<rect width="27.9181" height="20.101" fill="currentColor" transform="matrix(-1 0 0 1 143.553 0.211082)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -75,7 +75,8 @@
"@tanstack/solid-query": "^5.76.0",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0"
"three": "^0.176.0",
"valibot": "^1.1.0"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.25.4",

View File

@@ -29,7 +29,7 @@
box-shadow: 0.125rem 0.125rem 0 0 theme(colors.bg.inv.acc.3) inset;
&.ghost {
@apply bg-transparent border-none shadow-none;
@apply bg-transparent border-transparent shadow-none;
}
&:hover {
@@ -71,7 +71,7 @@
0.125rem 0.125rem 0 0 theme(colors.off.white) inset;
&.ghost {
@apply bg-transparent border-none shadow-none;
@apply bg-transparent border-transparent shadow-none;
}
&:hover {
@@ -138,6 +138,10 @@
transition: all 0.5s ease;
}
}
& > span.typography {
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
}
}
/* button group */

View File

@@ -1,5 +1,5 @@
hr {
@apply border-none outline-none bg-inv-2;
@apply border-none outline-none bg-inv-2 self-stretch;
&.inverted {
@apply bg-def-3;

View File

@@ -1,10 +1,15 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { Fieldset, FieldsetProps } from "@/src/components/Form/Fieldset";
import {
Fieldset,
FieldsetFieldProps,
FieldsetProps,
} from "@/src/components/Form/Fieldset";
import cx from "classnames";
import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/Form/Checkbox";
import { FieldProps } from "./Field";
import { HostFileInput } from "@/src/components/Form/HostFileInput";
const FieldsetExamples = (props: FieldsetProps) => (
<div class="flex flex-col gap-8">
@@ -22,7 +27,7 @@ const meta = {
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[1024px]": context.args.orientation == "horizontal",
"w-[512px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
@@ -40,7 +45,7 @@ export type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
legend: "Signup",
children: (props: FieldProps) => (
children: (props: FieldsetFieldProps) => (
<>
<TextInput
{...props}
@@ -59,6 +64,11 @@ export const Default: Story = {
label="Bio"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<HostFileInput
{...props}
label="Profile pic"
onSelectFile={async () => "/home/foo/bar/baz/fizz/buzz/bla/bizz"}
/>
<Checkbox {...props} label="Accept Terms" required={true} />
</>
),

View File

@@ -12,24 +12,28 @@ export type FieldsetFieldProps = Pick<
disabled?: boolean;
};
export interface FieldsetProps
extends Pick<FieldProps, "orientation" | "inverted"> {
export type FieldsetProps = Pick<FieldProps, "orientation" | "inverted"> & {
legend?: string;
disabled?: boolean;
error?: string;
children: (props: FieldsetFieldProps) => JSX.Element;
}
disabled?: boolean;
name?: string;
children: JSX.Element | ((props: FieldsetFieldProps) => JSX.Element);
};
export const Fieldset = (props: FieldsetProps) => {
const orientation = () => props.orientation || "vertical";
const [fieldProps] = splitProps(props, [
"orientation",
"inverted",
"disabled",
"error",
"children",
]);
const children = () =>
typeof props.children === "function"
? props.children(fieldProps)
: props.children;
return (
<fieldset
role="group"
@@ -51,7 +55,7 @@ export const Fieldset = (props: FieldsetProps) => {
</Typography>
</legend>
)}
<div class="fields">{props.children(fieldProps)}</div>
<div class="fields">{children()}</div>
{props.error && (
<div class="error" role="alert">
<Typography

View File

@@ -0,0 +1,11 @@
div.form-field.host-file {
button {
@apply w-fit;
}
&.horizontal {
button {
@apply grow max-w-[18rem];
}
}
}

View File

@@ -0,0 +1,131 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import cx from "classnames";
import {
HostFileInput,
HostFileInputProps,
} from "@/src/components/Form/HostFileInput";
const Examples = (props: HostFileInputProps) => (
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-8 p-8">
<HostFileInput {...props} />
<HostFileInput {...props} size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<HostFileInput {...props} inverted={true} />
<HostFileInput {...props} inverted={true} size="s" />
</div>
<div class="flex flex-col gap-8 p-8">
<HostFileInput {...props} orientation="horizontal" />
<HostFileInput {...props} orientation="horizontal" size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<HostFileInput {...props} inverted={true} orientation="horizontal" />
<HostFileInput
{...props}
inverted={true}
orientation="horizontal"
size="s"
/>
</div>
</div>
);
const meta = {
title: "Components/Form/HostFileInput",
component: Examples,
decorators: [
(Story: StoryObj, context: StoryContext<HostFileInputProps>) => {
return (
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[1024px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
<Story />
</div>
);
},
],
} satisfies Meta<HostFileInputProps>;
export default meta;
export type Story = StoryObj<typeof meta>;
export const Bare: Story = {
args: {
onSelectFile: async () => {
return "/home/github/clans/my-clan/foo/bar/baz/fizz/buzz";
},
input: {
placeholder: "e.g. 11/06/89",
},
},
};
export const Label: Story = {
args: {
...Bare.args,
label: "DOB",
},
};
export const Description: Story = {
args: {
...Label.args,
description: "The date you were born",
},
};
export const Required: Story = {
args: {
...Description.args,
required: true,
},
};
export const Tooltip: Story = {
args: {
...Required.args,
tooltip: "The day you came out of your momma",
},
};
export const Icon: Story = {
args: {
...Tooltip.args,
icon: "Checkmark",
},
};
export const Ghost: Story = {
args: {
...Icon.args,
ghost: true,
},
};
export const Invalid: Story = {
args: {
...Tooltip.args,
validationState: "invalid",
},
};
export const Disabled: Story = {
args: {
...Icon.args,
disabled: true,
},
};
export const ReadOnly: Story = {
args: {
...Icon.args,
readOnly: true,
defaultValue: "14/05/02",
},
};

View File

@@ -0,0 +1,111 @@
import {
TextField,
TextFieldInputProps,
TextFieldRootProps,
} from "@kobalte/core/text-field";
import cx from "classnames";
import { Label } from "./Label";
import { Button } from "../Button/Button";
import "./HostFileInput.css";
import { PolymorphicProps } from "@kobalte/core/polymorphic";
import { FieldProps } from "./Field";
import { Orienter } from "./Orienter";
import { createSignal } from "solid-js";
import { Tooltip } from "@kobalte/core/tooltip";
import { Typography } from "@/src/components/Typography/Typography";
export type HostFileInputProps = FieldProps &
TextFieldRootProps & {
onSelectFile: () => Promise<string>;
input?: PolymorphicProps<"input", TextFieldInputProps<"input">>;
};
export const HostFileInput = (props: HostFileInputProps) => {
const [value, setValue] = createSignal<string>(props.value || "");
let actualInputElement: HTMLInputElement | undefined;
const selectFile = async () => {
try {
console.log("selecting file", props.onSelectFile);
setValue(await props.onSelectFile());
actualInputElement?.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true }),
);
} catch (error) {
console.log("Error selecting file", error);
// todo work out how to display the error
}
};
return (
<TextField
class={cx("form-field", "host-file", props.size, props.orientation, {
inverted: props.inverted,
ghost: props.ghost,
})}
{...props}
>
<Orienter
orientation={props.orientation}
align={props.orientation == "horizontal" ? "center" : "start"}
>
<Label
labelComponent={TextField.Label}
descriptionComponent={TextField.Description}
{...props}
/>
<TextField.Input
{...props.input}
hidden={true}
value={value()}
ref={(el: HTMLInputElement) => {
actualInputElement = el; // Capture for local use
}}
/>
{!value() && (
<Button
hierarchy="secondary"
size={props.size}
startIcon="Folder"
onClick={selectFile}
disabled={props.disabled || props.readOnly}
>
No Selection
</Button>
)}
{value() && (
<Tooltip placement="top">
<Tooltip.Portal>
<Tooltip.Content class="tooltip-content">
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{value()}
</Typography>
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
<Tooltip.Trigger
as={Button}
hierarchy="secondary"
size={props.size}
startIcon="Folder"
onClick={selectFile}
disabled={props.disabled || props.readOnly}
>
{value()}
</Tooltip.Trigger>
</Tooltip>
)}
</Orienter>
</TextField>
);
};

View File

@@ -22,40 +22,3 @@ div.form-label {
}
}
}
div.tooltip-content {
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
max-width: min(calc(100vw - 16px), 380px);
transform-origin: var(--kb-tooltip-content-transform-origin);
animation: tooltipHide 250ms ease-in forwards;
&[data-expanded] {
animation: tooltipShow 250ms ease-out;
}
&.inverted {
@apply bg-def-2;
}
}
@keyframes tooltipShow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes tooltipHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -1,12 +1,11 @@
import { Show } from "solid-js";
import { Typography } from "@/src/components/Typography/Typography";
import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
import Icon from "@/src/components/Icon/Icon";
import { TextField } from "@kobalte/core/text-field";
import { Checkbox } from "@kobalte/core/checkbox";
import { Combobox } from "@kobalte/core/combobox";
import "./Label.css";
import cx from "classnames";
export type Size = "default" | "s";
@@ -49,31 +48,27 @@ export const Label = (props: LabelProps) => {
{props.label}
</Typography>
{props.tooltip && (
<KTooltip placement="top">
<KTooltip.Trigger>
<Tooltip
placement="top"
inverted={props.inverted}
trigger={
<Icon
icon="Info"
color="tertiary"
inverted={props.inverted}
size={props.size == "default" ? "0.85em" : "0.75rem"}
/>
<KTooltip.Portal>
<KTooltip.Content
class={cx("tooltip-content", { inverted: props.inverted })}
>
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{props.tooltip}
</Typography>
<KTooltip.Arrow />
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip.Trigger>
</KTooltip>
}
>
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{props.tooltip}
</Typography>
</Tooltip>
)}
</props.labelComponent>
{props.description && (

View File

@@ -5,7 +5,7 @@ div.orienter {
}
&.horizontal {
@apply flex-row justify-start;
@apply flex-row justify-between gap-0;
& > div.form-label {
@apply w-1/2 shrink;

View File

@@ -102,6 +102,7 @@ export type IconVariant = keyof typeof icons;
const viewBoxes: Partial<Record<IconVariant, string>> = {
ClanIcon: "0 0 72 89",
Offline: "0 0 38 27",
Cursor: "0 0 35 42",
};
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {

View File

@@ -0,0 +1,88 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { Component, For } from "solid-js";
import { Logo, LogoProps, LogoVariant } from "./Logo";
import cx from "classnames";
const variants: LogoVariant[] = ["Clan", "Darknet"];
const LogoExamples: Component<LogoProps> = (props) => (
<div class="grid grid-cols-6 items-center gap-4">
<For each={variants}>{(item) => <Logo {...props} variant={item} />}</For>
</div>
);
const meta: Meta<LogoProps> = {
title: "Components/Logo",
component: LogoExamples,
decorators: [
(Story: StoryObj, context: StoryContext<LogoProps>) => (
<div class={cx(context.args.inverted || false ? "bg-inv-acc-3" : "")}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<LogoProps>;
export const Default: Story = {};
export const Primary: Story = {
args: {
color: "primary",
},
};
export const Secondary: Story = {
args: {
color: "secondary",
},
};
export const Tertiary: Story = {
args: {
color: "tertiary",
},
};
export const Quaternary: Story = {
args: {
color: "quaternary",
},
};
export const PrimaryInverted: Story = {
args: {
...Primary.args,
inverted: true,
},
};
export const SecondaryInverted: Story = {
args: {
...Secondary.args,
inverted: true,
},
};
export const TertiaryInverted: Story = {
args: {
...Tertiary.args,
inverted: true,
},
};
export const QuaternaryInverted: Story = {
args: {
...Quaternary.args,
inverted: true,
},
};
export const Inverted: Story = {
args: {
inverted: true,
},
};

View File

@@ -0,0 +1,40 @@
import Clan from "@/logos/clan.svg";
import Darknet from "@/logos/darknet.svg";
import { Dynamic } from "solid-js/web";
import { Color, fgClass } from "@/src/components/colors";
import { JSX, splitProps } from "solid-js";
import cx from "classnames";
const logos = {
Clan,
Darknet,
};
export type LogoVariant = keyof typeof logos;
export interface LogoProps extends JSX.SvgSVGAttributes<SVGElement> {
class?: string;
variant: LogoVariant;
color?: Color;
inverted?: boolean;
}
export const Logo = (props: LogoProps) => {
const [local, iconProps] = splitProps(props, [
"variant",
"color",
"class",
"inverted",
]);
const Logo = logos[local.variant];
return (
<Dynamic
component={Logo}
class={cx("icon", local.class, fgClass(local.color, local.inverted), {
inverted: local.inverted,
})}
data-logo-name={local.variant}
/>
);
};

View File

@@ -1,5 +1,5 @@
div.modal-content {
@apply max-w-[512px];
@apply min-w-[320px] max-w-[512px];
@apply rounded-md;
/* todo replace with a theme() color */
@@ -12,7 +12,7 @@ div.modal-content {
@apply border border-def-2 rounded-tl-md rounded-tr-md;
@apply border-b-def-3;
& > .title {
& > .modal-title {
@apply mx-auto;
}
}

View File

@@ -2,7 +2,7 @@ import { TagProps } from "@/src/components/Tag/Tag";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { fn } from "storybook/test";
import { Modal, ModalContext, ModalProps } from "@/src/components/Modal/Modal";
import { Fieldset } from "@/src/components/Form/Fieldset";
import { Fieldset, FieldsetFieldProps } from "@/src/components/Form/Fieldset";
import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/Form/Checkbox";
@@ -24,7 +24,7 @@ export const Default: Story = {
children: ({ close }: ModalContext) => (
<form class="flex flex-col gap-5">
<Fieldset legend="General">
{(props) => (
{(props: FieldsetFieldProps) => (
<>
<TextInput
{...props}

View File

@@ -3,6 +3,7 @@ import { Dialog as KDialog } from "@kobalte/core/dialog";
import "./Modal.css";
import { Typography } from "../Typography/Typography";
import Icon from "../Icon/Icon";
import cx from "classnames";
export interface ModalContext {
close(): void;
@@ -13,6 +14,8 @@ export interface ModalProps {
title: string;
onClose: () => void;
children: (ctx: ModalContext) => JSX.Element;
mount?: Node;
class?: string;
}
export const Modal = (props: ModalProps) => {
@@ -20,18 +23,33 @@ export const Modal = (props: ModalProps) => {
return (
<KDialog id={props.id} open={open()} modal={true}>
<KDialog.Portal>
<KDialog.Content class="modal-content">
<KDialog.Portal mount={props.mount}>
<KDialog.Content class={cx("modal-content", props.class)}>
<div class="header">
<Typography class="title" hierarchy="label" family="mono" size="xs">
<Typography
class="modal-title"
hierarchy="label"
family="mono"
size="xs"
>
{props.title}
</Typography>
<KDialog.CloseButton onClick={() => setOpen(false)}>
<KDialog.CloseButton
onClick={() => {
setOpen(false);
props.onClose();
}}
>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
</div>
<div class="body">
{props.children({ close: () => setOpen(false) })}
{props.children({
close: () => {
setOpen(false);
props.onClose();
},
})}
</div>
</KDialog.Content>
</KDialog.Portal>

View File

@@ -14,35 +14,35 @@ import { StoryContext } from "@kachurun/storybook-solid-vite";
const sidebarNavProps: SidebarNavProps = {
clanLinks: [
{ label: "Brian's Clan", path: "/clan/1" },
{ label: "Dave's Clan", path: "/clan/2" },
{ label: "Mic92's Clan", path: "/clan/3" },
{ label: "Brian's Clan", path: "/clans/1" },
{ label: "Dave's Clan", path: "/clans/2" },
{ label: "Mic92's Clan", path: "/clans/3" },
],
clanDetail: {
label: "Brian's Clan",
settingsPath: "/clan/1/settings",
settingsPath: "/clans/1/settings",
machines: [
{
label: "Backup & Home",
path: "/clan/1/machine/backup",
path: "/clans/1/machine/backup",
serviceCount: 3,
status: "Online",
},
{
label: "Raspberry Pi",
path: "/clan/1/machine/pi",
path: "/clans/1/machine/pi",
serviceCount: 1,
status: "Offline",
},
{
label: "Mom's Laptop",
path: "/clan/1/machine/moms-laptop",
path: "/clans/1/machine/moms-laptop",
serviceCount: 2,
status: "Installed",
},
{
label: "Dad's Laptop",
path: "/clan/1/machine/dads-laptop",
path: "/clans/1/machine/dads-laptop",
serviceCount: 4,
status: "Not Installed",
},
@@ -52,10 +52,10 @@ const sidebarNavProps: SidebarNavProps = {
{
label: "Tools",
links: [
{ label: "Borgbackup", path: "/clan/1/service/borgbackup" },
{ label: "Syncthing", path: "/clan/1/service/syncthing" },
{ label: "Mumble", path: "/clan/1/service/mumble" },
{ label: "Minecraft", path: "/clan/1/service/minecraft" },
{ label: "Borgbackup", path: "/clans/1/service/borgbackup" },
{ label: "Syncthing", path: "/clans/1/service/syncthing" },
{ label: "Mumble", path: "/clans/1/service/mumble" },
{ label: "Minecraft", path: "/clans/1/service/minecraft" },
],
},
{
@@ -81,7 +81,7 @@ const meta: Meta<RouteSectionProps> = {
component: SidebarNav,
render: (_: never, context: StoryContext<SidebarNavProps>) => {
const history = createMemoryHistory();
history.set({ value: "/clan/1/machine/backup" });
history.set({ value: "/clans/1/machine/backup" });
return (
<div style="height: 670px;">
@@ -93,7 +93,7 @@ const meta: Meta<RouteSectionProps> = {
</Suspense>
)}
>
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
<Route path="/clans/1/machine/backup" component={(props) => <></>} />
</MemoryRouter>
</div>
);

View File

@@ -0,0 +1,30 @@
div.toolbar {
@apply flex flex-row items-center justify-center gap-1.5 p-1 size-fit rounded-md self-stretch;
border: 0.0625rem solid theme(colors.off.toolbar_border);
background: linear-gradient(
180deg,
theme(colors.bg.inv.3) 0%,
theme(colors.bg.inv.4) 100%
);
& > hr {
@apply h-full min-h-8;
}
& > button.toolbar-button {
@apply w-6 h-6 p-1 rounded-[0.25rem];
&:hover {
@apply bg-inv-1;
}
&:active {
@apply bg-inv-4;
}
&.selected {
@apply bg-bg-semantic-success-4;
}
}
}

View File

@@ -0,0 +1,27 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Toolbar, ToolbarProps } from "@/src/components/Toolbar/Toolbar";
import { Divider } from "@/src/components/Divider/Divider";
import { ToolbarButton } from "./ToolbarButton";
const meta: Meta<ToolbarProps> = {
title: "Components/Toolbar",
component: Toolbar,
};
export default meta;
type Story = StoryObj<ToolbarProps>;
export const Default: Story = {
args: {
children: (
<>
<ToolbarButton name="select" icon="Cursor" />
<ToolbarButton name="new-machine" icon="NewMachine" />
<Divider orientation="vertical" />
<ToolbarButton name="modules" icon="Modules" selected={true} />
<ToolbarButton name="ai" icon="AI" />
</>
),
},
};

View File

@@ -0,0 +1,14 @@
import "./Toolbar.css";
import { JSX } from "solid-js";
export interface ToolbarProps {
children: JSX.Element;
}
export const Toolbar = (props: ToolbarProps) => {
return (
<div class="toolbar" role="toolbar" aria-orientation="horizontal">
{props.children}
</div>
);
};

View File

@@ -0,0 +1,22 @@
import "./Toolbar.css";
import cx from "classnames";
import { Button } from "@kobalte/core/button";
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import type { JSX } from "solid-js";
export interface ToolbarButtonProps
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
icon: IconVariant;
selected?: boolean;
}
export const ToolbarButton = (props: ToolbarButtonProps) => {
return (
<Button
class={cx("toolbar-button", { selected: props.selected })}
{...props}
>
<Icon icon={props.icon} inverted={!props.selected} />
</Button>
);
};

View File

@@ -0,0 +1,9 @@
div.tooltip-content {
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
max-width: min(calc(100vw - 16px), 380px);
&.inverted {
@apply bg-def-2;
}
}

View File

@@ -0,0 +1,40 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Tooltip, TooltipProps } from "@/src/components/Tooltip/Tooltip";
import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button";
const meta: Meta<TooltipProps> = {
title: "Components/Tooltip",
component: Tooltip,
decorators: [
(Story: StoryObj<TooltipProps>) => (
<div class="p-16">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<TooltipProps>;
export const Default: Story = {
args: {
placement: "top",
inverted: false,
trigger: <Button hierarchy="primary">Trigger</Button>,
children: (
<Typography hierarchy="body" size="xs" inverted={true} weight="medium">
Your Clan is being created
</Typography>
),
},
};
export const AnimateBounce: Story = {
args: {
...Default.args,
animation: "bounce",
},
};

View File

@@ -0,0 +1,34 @@
import "./Tooltip.css";
import {
Tooltip as KTooltip,
TooltipRootProps as KTooltipRootProps,
} from "@kobalte/core/tooltip";
import cx from "classnames";
import { JSX } from "solid-js";
export interface TooltipProps extends KTooltipRootProps {
inverted?: boolean;
trigger: JSX.Element;
children: JSX.Element;
animation?: "bounce";
}
export const Tooltip = (props: TooltipProps) => {
return (
<KTooltip {...props}>
<KTooltip.Trigger>{props.trigger}</KTooltip.Trigger>
<KTooltip.Portal>
<KTooltip.Content
class={cx("tooltip-content", {
inverted: props.inverted,
"animate-bounce": props.animation == "bounce",
})}
>
{props.placement == "bottom" && <KTooltip.Arrow />}
{props.children}
{props.placement == "top" && <KTooltip.Arrow />}
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip>
);
};

View File

@@ -148,6 +148,18 @@
line-height: 116%;
letter-spacing: 0.06rem;
}
&.size-xl {
font-size: 2.5rem;
line-height: 104%;
letter-spacing: -0.06rem;
}
&.size-xxl {
font-size: 3rem;
line-height: 104%;
letter-spacing: -0.06rem;
}
}
&.teaser {
@@ -161,4 +173,16 @@
letter-spacing: -0.06rem;
}
}
&.align-left {
text-align: left;
}
&.align-center {
text-align: center;
}
&.align-right {
text-align: right;
}
}

View File

@@ -28,6 +28,8 @@ interface SizeForHierarchy {
default: string;
m: string;
l: string;
xl: string;
xxl: string;
};
title: {
default: string;
@@ -52,6 +54,8 @@ const sizeHierarchyMap: SizeForHierarchy = {
default: cx("size-default"),
m: cx("size-m"),
l: cx("size-l"),
xl: cx("size-xl"),
xxl: cx("size-xxl"),
},
title: {
default: cx("size-default"),
@@ -97,6 +101,7 @@ interface _TypographyProps<H extends Hierarchy> {
tag?: Tag;
class?: string;
transform?: Transform;
align?: "left" | "center" | "right";
}
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
@@ -106,6 +111,7 @@ export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
const size = () => sizeHierarchyMap[props.hierarchy][props.size] as string;
const weight = () => weightMap[props.weight || "normal"];
const color = () => fgClass(props.color, props.inverted);
const align = () => `align-${props.align || "left"}`;
return (
<Dynamic
@@ -116,6 +122,7 @@ export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
weight(),
size(),
color(),
align(),
props.transform,
props.class,
)}

View File

@@ -0,0 +1,92 @@
import { API } from "@/api/API";
import { Schema as Inventory } from "@/api/Inventory";
type OperationNames = keyof API;
type Services = NonNullable<Inventory["services"]>;
type ServiceNames = keyof Services;
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
Services[T]
>[string];
export type SuccessQuery<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "success" }
>;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
interface SendHeaderType {
logging?: { group_path: string[] };
}
interface BackendSendType<K extends OperationNames> {
body: OperationArgs<K>;
header?: SendHeaderType;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ReceiveHeaderType {}
interface BackendReturnType<K extends OperationNames> {
body: OperationResponse<K>;
header: ReceiveHeaderType;
}
/**
* Interface representing an API call with a unique identifier, result promise, and cancellation capability.
*
* @template K - A generic type parameter extending the set of operation names.
*
* @property {string} uuid - A unique identifier for the API call.
* @property {Promise<BackendReturnType<K>>} result - A promise that resolves to the return type of the backend operation.
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
*/
export interface ApiCall<K extends OperationNames> {
uuid: string;
result: Promise<OperationResponse<K>>;
cancel: () => Promise<void>;
}
export const callApi = <K extends OperationNames>(
method: K,
args: OperationArgs<K>,
backendOpts?: SendHeaderType,
): ApiCall<K> => {
// if window[method] does not exist, throw an error
if (!(method in window)) {
console.error(`Method ${method} not found on window object`);
return {
uuid: "",
result: Promise.reject(`Method ${method} not found on window object`),
cancel: () => Promise.resolve(),
};
}
const req: BackendSendType<OperationNames> = {
body: args,
header: backendOpts,
};
const result = (
window as unknown as Record<
OperationNames,
(
args: BackendSendType<OperationNames>,
) => Promise<BackendReturnType<OperationNames>>
>
)[method](req) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (result as any)._webviewMessageId as string;
return {
uuid: op_key,
result: result.then(({ body }) => body),
cancel: async () => {
console.log("Cancelling api call: ", op_key);
await callApi("delete_task", { task_id: op_key }).result;
},
};
};

View File

@@ -0,0 +1,57 @@
import { callApi } from "@/src/hooks/api";
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
import { Params, Navigator, useParams } from "@solidjs/router";
export const selectClanFolder = async () => {
const req = callApi("get_clan_folder", {});
const res = await req.result;
if (res.status === "error") {
throw new Error(res.errors[0].message);
}
if (res.status === "success" && res.data) {
const { identifier: uri } = res.data;
addClanURI(uri);
setActiveClanURI(uri);
return uri;
}
throw new Error("Illegal state exception");
};
export const navigateToClan = (navigate: Navigator, uri: string) => {
navigate("/clans/" + window.btoa(uri));
};
export const clanURIParam = (params: Params) => {
return window.atob(params.clanURI);
};
export function useClanURI(opts: { force: true }): string;
export function useClanURI(opts: { force: boolean }): string | null;
export function useClanURI(
opts: { force: boolean } = { force: false },
): string | null {
const maybe = () => {
const params = useParams();
if (!params.clanURI) {
return null;
}
const clanURI = clanURIParam(params);
if (!clanURI) {
throw new Error(
"Could not decode clan URI from params: " + params.clanURI,
);
}
return clanURI;
};
const uri = maybe();
if (!uri && opts.force) {
throw new Error(
"ClanURI is not set. Use this function only within contexts, where clanURI is guaranteed to have been set.",
);
}
return uri;
}

View File

@@ -2,8 +2,10 @@
import { render } from "solid-js/web";
import "./index.css";
import { QueryClient } from "@tanstack/solid-query";
import { CubeScene } from "./scene/cubes";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { Routes } from "@/src/routes";
import { Router } from "@solidjs/router";
import { Layout } from "@/src/routes/Layout";
export const client = new QueryClient();
@@ -20,4 +22,11 @@ if (import.meta.env.DEV) {
await import("solid-devtools");
}
render(() => <CubeScene />, root!);
render(
() => (
<QueryClientProvider client={client}>
<Router root={Layout}>{Routes}</Router>
</QueryClientProvider>
),
root!,
);

View File

@@ -0,0 +1,31 @@
import { useQuery, UseQueryResult } from "@tanstack/solid-query";
import { callApi, SuccessData } from "../hooks/api";
export type ListMachines = SuccessData<"list_machines">;
export type MachinesQueryResult = UseQueryResult<ListMachines>;
interface MachinesQueryParams {
clanURI: string | null;
}
export const useMachinesQuery = (props: MachinesQueryParams) =>
useQuery<ListMachines>(() => ({
queryKey: ["clans", props.clanURI, "machines"],
enabled: !!props.clanURI,
queryFn: async () => {
if (!props.clanURI) {
return {};
}
const api = callApi("list_machines", {
flake: {
identifier: props.clanURI,
},
});
const result = await api.result;
if (result.status === "error") {
console.error("Error fetching machines:", result.errors);
return {};
}
return result.data;
},
}));

View File

@@ -0,0 +1,13 @@
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}
.create-backdrop {
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
-webkit-backdrop-filter: blur(4px);
}
.create-modal {
@apply min-w-96;
}

View File

@@ -0,0 +1,231 @@
import { RouteSectionProps } from "@solidjs/router";
import { Component, JSX, Show, createSignal } from "solid-js";
import { useClanURI } from "@/src/hooks/clan";
import { CubeScene } from "@/src/scene/cubes";
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
import { callApi } from "@/src/hooks/api";
import { store, setStore } from "@/src/stores/clan";
import { produce } from "solid-js/store";
import { Button } from "@/src/components/Button/Button";
import { Splash } from "@/src/scene/splash";
import cx from "classnames";
import "./Clan.css";
import { Modal } from "@/src/components/Modal/Modal";
import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid";
export const Clan: Component<RouteSectionProps> = (props) => {
return (
<>
<div
style={{
position: "absolute",
top: 0,
}}
>
{props.children}
</div>
<ClanSceneController />
</>
);
};
interface CreateFormValues extends FieldValues {
name: string;
}
interface MockProps {
onClose: () => void;
onSubmit: (formValues: CreateFormValues) => void;
}
const MockCreateMachine = (props: MockProps) => {
let container: Node;
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
return (
<div ref={(el) => (container = el)} class="create-backdrop">
<Modal
mount={container!}
onClose={() => {
reset(form);
props.onClose();
}}
class="create-modal"
title="Create Machine"
>
{() => (
<Form class="flex flex-col" onSubmit={props.onSubmit}>
<Field name="name">
{(field, props) => (
<>
<TextInput
{...field}
label="Name"
size="s"
required={true}
input={{ ...props, placeholder: "name" }}
/>
</>
)}
</Field>
<div class="flex w-full items-center justify-end gap-4">
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
Cancel
</Button>
<Button
size="s"
type="submit"
hierarchy="primary"
onClick={close}
>
Create
</Button>
</div>
</Form>
)}
</Modal>
</div>
);
};
const ClanSceneController = () => {
const clanURI = useClanURI({ force: true });
const [dialogHandlers, setDialogHandlers] = createSignal<{
resolve: ({ id }: { id: string }) => void;
reject: (err: unknown) => void;
} | null>(null);
const onCreate = async (): Promise<{ id: string }> => {
return new Promise((resolve, reject) => {
setShowModal(true);
setDialogHandlers({ resolve, reject });
});
};
const sendCreate = async (values: CreateFormValues) => {
const api = callApi("create_machine", {
opts: {
clan_dir: {
identifier: clanURI,
},
machine: {
name: values.name,
},
},
});
const res = await api.result;
if (res.status === "error") {
// TODO: Handle displaying errors
console.error("Error creating machine:");
// Important: rejects the promise
throw new Error(res.errors[0].message);
}
return { id: values.name };
};
const [showModal, setShowModal] = createSignal(false);
return (
<SceneDataProvider clanURI={clanURI}>
{({ query }) => {
return (
<>
<Show when={showModal()}>
<MockCreateMachine
onClose={() => {
setShowModal(false);
dialogHandlers()?.reject(new Error("User cancelled"));
}}
onSubmit={async (values) => {
try {
const result = await sendCreate(values);
dialogHandlers()?.resolve(result);
setShowModal(false);
} catch (err) {
dialogHandlers()?.reject(err);
setShowModal(false);
}
}}
/>
</Show>
<div
class="flex flex-row"
style={{ position: "absolute", top: "10px", left: "10px" }}
>
<Button
ghost
onClick={() => {
setStore(
produce((s) => {
for (const machineId in s.sceneData[clanURI]) {
// Reset the position of each machine to [0, 0]
s.sceneData[clanURI] = {}; // Clear the entire object
// delete s.sceneData[clanURI][machineId];
}
}),
);
}}
>
Reset Store
</Button>
<Button
ghost
onClick={() => {
console.log("Refetching API");
query.refetch();
}}
>
Refetch API
</Button>
</div>
{/* TODO: Add minimal display time */}
<div class={cx({ "fade-out": !query.isLoading })}>
<Splash />
</div>
<CubeScene
isLoading={query.isLoading}
cubesQuery={query}
onCreate={onCreate}
sceneStore={() => {
const clanURI = useClanURI({ force: true });
return store.sceneData?.[clanURI];
}}
setMachinePos={(machineId: string, pos: [number, number]) => {
console.log("calling setStore", machineId, pos);
setStore(
produce((s) => {
if (!s.sceneData) {
s.sceneData = {};
}
if (!s.sceneData[clanURI]) {
s.sceneData[clanURI] = {};
}
if (!s.sceneData[clanURI][machineId]) {
s.sceneData[clanURI][machineId] = { position: pos };
} else {
s.sceneData[clanURI][machineId].position = pos;
}
}),
);
}}
/>
</>
);
}}
</SceneDataProvider>
);
};
const SceneDataProvider = (props: {
clanURI: string | null;
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element;
}) => {
const machinesQuery = useMachinesQuery({ clanURI: props.clanURI });
// This component can be used to provide scene data or context if needed
return props.children({ query: machinesQuery });
};

View File

@@ -0,0 +1,18 @@
import { Component } from "solid-js";
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import { activeClanURI } from "@/src/stores/clan";
import { navigateToClan } from "@/src/hooks/clan";
export const Layout: Component<RouteSectionProps> = (props) => {
const navigate = useNavigate();
// check for an active clan uri and redirect to it on first load
const activeURI = activeClanURI();
if (!props.location.pathname.startsWith("/clans/") && activeURI) {
navigateToClan(navigate, activeURI);
} else {
navigate("/");
}
return <div class="size-full h-screen">{props.children}</div>;
};

View File

@@ -0,0 +1,663 @@
div.creating {
@apply flex flex-col items-center justify-center;
div.scene {
width: 400px;
height: 400px;
perspective: 1000px;
/*background: red;*/
& > .frame {
position: relative;
top: 100px;
left: 65px;
width: 200px;
height: 200px;
/*background: green;*/
/*transform: rotate3d(-2, -2, 1, 45deg);*/
transform: rotate3d(-1.5, -2, 0.5, 45deg);
transform-style: preserve-3d;
& > .cube {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
transform-style: preserve-3d;
.cube-face {
position: absolute;
width: 100px;
height: 100px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.56) 0%,
rgba(255, 255, 255, 0) 100%
);
border: 1px #10191a solid;
opacity: 1;
&.front {
transform: rotateY(0deg) translateZ(50px);
}
&.right {
transform: rotateY(90deg) translateZ(50px);
}
&.back {
transform: rotateY(180deg) translateZ(50px);
}
&.left {
transform: rotateY(-90deg) translateZ(50px);
}
&.top {
transform: rotateX(90deg) translateZ(50px);
}
&.bottom {
transform: rotateX(-90deg) translateZ(50px);
}
}
&.state-1 {
animation: anim-cube-1-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-1-1 {
transform: translateZ(-120px);
animation: anim-cube-1-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-2 {
left: 120px;
animation: anim-cube-2-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-2-2 {
left: 120px;
transform: translateZ(-120px);
animation: anim-cube-2-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-3 {
top: 120px;
animation: anim-cube-3-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-3-3 {
top: 120px;
transform: translateZ(-120px);
animation: anim-cube-3-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-4 {
top: 120px;
left: 120px;
animation: anim-cube-4-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-4-4 {
top: 120px;
left: 120px;
transform: translateZ(-120px);
animation: anim-cube-4-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
}
}
}
}
@keyframes anim-cube-1-1 {
/* STEP 1 */
0% {
left: 0px;
transform: translateZ(0px);
}
2.083% {
left: -40px;
transform: translateZ(0px);
}
16.666% {
left: -40px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
left: 0;
transform: translateZ(0px);
}
33.332% {
left: 0;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
left: 0;
transform: translateZ(40px);
}
49.998% {
left: 0;
transform: translateZ(40px);
}
/* STEP 4 */
52.081% {
left: 0;
transform: translateZ(0);
}
66.664% {
left: 0;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
left: -60px;
transform: translateZ(60px);
}
83.33% {
left: -60px;
transform: translateZ(60px);
}
/* Step 6 */
85.413% {
left: 0px;
transform: translateZ(0px);
}
100% {
left: 0px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-2-1 {
/* STEP 1 */
0% {
left: 120px;
transform: translateZ(0px);
}
2.083% {
left: 180px;
transform: translateZ(0px);
}
16.666% {
left: 180px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
left: 120px;
transform: translateZ(0px);
}
33.332% {
left: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
left: 240px;
transform: translateZ(120px);
}
49.998% {
left: 240px;
transform: translateZ(120px);
}
/* STEP 4 */
52.081% {
left: 120px;
transform: translateZ(0);
}
66.664% {
left: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
left: 60px;
transform: translateZ(60px);
}
83.33% {
left: 60px;
transform: translateZ(60px);
}
/* Step 6 */
85.413% {
left: 120px;
transform: translateZ(0px);
}
100% {
left: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-3-1 {
/* STEP 1 */
0% {
top: 120px;
transform: translateZ(0px);
}
2.083% {
top: 220px;
transform: translateZ(0px);
}
16.666% {
top: 220px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
top: 120px;
transform: translateZ(0px);
}
33.332% {
top: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
top: 120px;
transform: translateZ(40px);
}
49.998% {
top: 120px;
transform: translateZ(40px);
}
/* STEP 4 */
52.081% {
top: 120px;
transform: translateZ(0);
}
66.664% {
top: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
top: 180px;
transform: translateZ(80px);
}
83.33% {
top: 180px;
transform: translateZ(80px);
}
/* Step 6 */
85.413% {
top: 120px;
transform: translateZ(0px);
}
100% {
top: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-4-1 {
/* STEP 1 */
0% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
2.083% {
top: 220px;
left: 180px;
transform: translateZ(0px);
}
16.666% {
top: 220px;
left: 180px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
33.332% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
top: 120px;
left: 240px;
transform: translateZ(120px);
}
49.998% {
top: 120px;
left: 240px;
transform: translateZ(120px);
}
/* STEP 4 */
52.081% {
top: 120px;
left: 120px;
transform: translateZ(0);
}
66.664% {
top: 120px;
left: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
top: 180px;
left: 260px;
transform: translateZ(80px);
}
83.33% {
top: 180px;
left: 260px;
transform: translateZ(80px);
}
/* Step 6 */
85.413% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
100% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-1-2 {
/* STEP 1 */
0% {
left: 0px;
transform: translateZ(-120px);
}
2.083% {
left: -40px;
transform: translateZ(-120px);
}
16.666% {
left: -40px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
left: 0px;
transform: translateZ(-120px);
}
33.332% {
left: 0px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
left: 0px;
transform: translateZ(-200px);
}
49.998% {
left: 0px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
left: 0px;
transform: translateZ(-120px);
}
66.664% {
left: 0px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
left: -60px;
transform: translateZ(-180px);
}
83.33% {
left: -60px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
left: 0px;
transform: translateZ(-120px);
}
100% {
left: 0px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-2-2 {
/* STEP 1 */
0% {
left: 120px;
transform: translateZ(-120px);
}
2.083% {
left: 180px;
transform: translateZ(-120px);
}
16.666% {
left: 180px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
left: 120px;
transform: translateZ(-120px);
}
33.332% {
left: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
left: 240px;
transform: translateZ(-200px);
}
49.998% {
left: 240px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
left: 120px;
transform: translateZ(-120px);
}
66.664% {
left: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
left: 60px;
transform: translateZ(-180px);
}
83.33% {
left: 60px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
left: 120px;
transform: translateZ(-120px);
}
100% {
left: 120px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-3-2 {
/* STEP 1 */
0% {
top: 120px;
transform: translateZ(-120px);
}
2.083% {
top: 220px;
transform: translateZ(-120px);
}
16.666% {
top: 220px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
top: 120px;
transform: translateZ(-120px);
}
33.332% {
top: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
top: 120px;
transform: translateZ(-200px);
}
49.998% {
top: 120px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
top: 120px;
transform: translateZ(-120px);
}
66.664% {
top: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
top: 180px;
transform: translateZ(-180px);
}
83.33% {
top: 180px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
top: 120px;
transform: translateZ(-120px);
}
100% {
top: 120px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-4-2 {
/* STEP 1 */
0% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
2.083% {
top: 220px;
left: 180px;
transform: translateZ(-120px);
}
16.666% {
top: 220px;
left: 180px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
33.332% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
top: 120px;
left: 240px;
transform: translateZ(-200px);
}
49.998% {
top: 120px;
left: 240px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
66.664% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
top: 180px;
left: 260px;
transform: translateZ(-180px);
}
83.33% {
top: 180px;
left: 260px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
100% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
}

View File

@@ -0,0 +1,90 @@
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
import { Typography } from "@/src/components/Typography/Typography";
import "./Creating.css";
export const Creating = () => (
<div class="creating">
<Tooltip open={true} placement="top" trigger={<div />}>
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
Your Clan is being created
</Typography>
</Tooltip>
<div class="scene">
<div class="frame">
<div id="cube-1" class="cube state-1">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-2" class="cube state-2">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-3" class="cube state-3">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-4" class="cube state-4">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-1-1" class="cube state-1-1">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-2-2" class="cube state-2-2">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-3-3" class="cube state-3-3">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-4-4" class="cube state-4-4">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,84 @@
main#welcome {
@apply absolute top-0 left-0;
@apply flex items-center justify-center;
@apply min-h-screen w-full;
div.background {
.layer-1 {
@apply -z-30;
background:
url("./background.png") 0 -69.032px / 100% 119.049% no-repeat,
url("./background.png") 50% / cover no-repeat;
}
.layer-2 {
@apply -z-20;
background: #103131;
mix-blend-mode: screen;
}
.layer-3 {
@apply -z-10;
background: #749095;
mix-blend-mode: soft-light;
}
.layer-1,
.layer-2,
.layer-3 {
@apply absolute top-0 left-0 w-full h-full;
}
svg[data-logo-name="Darknet"] {
@apply w-52;
@apply absolute top-28 left-1/2 transform -translate-x-1/2;
}
svg[data-logo-name="Clan"] {
@apply w-16;
@apply absolute bottom-28 left-1/2 transform -translate-x-1/2;
}
div.darknet-info {
@apply absolute bottom-[6.5rem] left-12;
@apply flex flex-col gap-y-2;
span.darknet-label {
color: theme(colors.off.darknet_label);
}
span.darknet-name {
color: theme(colors.off.darknet_name);
}
}
}
& > div.container {
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
& > div.welcome {
@apply flex flex-col min-w-80 gap-y-6;
& > div.separator {
@apply grid grid-cols-3 grid-rows-1 gap-x-4 items-center;
}
}
& > div.setup {
@apply flex flex-col w-[33rem] gap-y-5;
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
& > div.header {
@apply flex items-center justify-start gap-x-2;
}
form {
@apply flex flex-col gap-y-5;
& > div.form-controls {
@apply flex justify-end pt-6;
}
}
}
}
}

View File

@@ -0,0 +1,367 @@
import {
Accessor,
Component,
createSignal,
Match,
Setter,
Show,
Switch,
} from "solid-js";
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import "./Onboarding.css";
import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button";
import { Alert } from "@/src/components/Alert/Alert";
import { Divider } from "@/src/components/Divider/Divider";
import { Logo } from "@/src/components/Logo/Logo";
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
import { activeClanURI, addClanURI, setActiveClanURI } from "@/src/stores/clan";
import {
createForm,
FormStore,
getError,
getErrors,
getValue,
SubmitHandler,
valiForm,
} from "@modular-forms/solid";
import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/Form/TextArea";
import { Fieldset } from "@/src/components/Form/Fieldset";
import * as v from "valibot";
import { HostFileInput } from "@/src/components/Form/HostFileInput";
import { callApi } from "@/src/hooks/api";
import { Creating } from "./Creating";
type State = "welcome" | "setup" | "creating";
const SetupSchema = v.object({
name: v.pipe(
v.string(),
v.nonEmpty("Please enter a name."),
v.regex(
new RegExp("^[a-zA-Z0-9_\\-]+$"),
"Name must be alphanumeric and can contain underscores and dashes, without spaces.",
),
),
description: v.pipe(v.string(), v.nonEmpty("Please describe your clan.")),
directory: v.pipe(
// initial value is undefined, and I can't see how to handle this better in valibot, so for now when the type
// is incorrect we treat it as empty
v.string("Please select a directory."),
v.nonEmpty("Please select a directory."),
),
});
type SetupForm = v.InferInput<typeof SetupSchema>;
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
<div class="background">
<div class="layer-1" />
<div class="layer-2" />
<div class="layer-3" />
<Logo variant="Darknet" inverted={true} />
<Logo variant="Clan" inverted={true} />
<Show when={props.state === "setup"}>
<div class="darknet-info">
<Typography
class="darknet-label"
hierarchy="label"
family="mono"
size="default"
color="inherit"
weight="medium"
inverted={true}
>
Your Darknet:
</Typography>
<Typography
class="darknet-name"
hierarchy="teaser"
size="default"
color="inherit"
weight="medium"
inverted={true}
>
{getValue(props.form, "name")}
</Typography>
</div>
</Show>
</div>
);
const welcome = (props: {
setState: Setter<State>;
welcomeError: Accessor<string | undefined>;
setWelcomeError: Setter<string | undefined>;
}) => {
const navigate = useNavigate();
const selectFolder = async () => {
const uri = await selectClanFolder();
navigateToClan(navigate, uri);
};
return (
<div class="welcome">
<Typography
hierarchy="headline"
size="xxl"
weight="bold"
align="center"
inverted={true}
>
Build your <br />
own darknet
</Typography>
{props.welcomeError() && (
<Alert
type="error"
icon="Info"
title="Your Clan creation failed"
description={props.welcomeError() || ""}
/>
)}
<Button
hierarchy="secondary"
onClick={() => {
// reset welcome error
props.setWelcomeError(undefined);
// move to next step
props.setState("setup");
}}
>
Start building
</Button>
<div class="separator">
<Divider orientation="horizontal" inverted={true} />
<Typography
hierarchy="body"
size="s"
weight="medium"
inverted={true}
align="center"
>
or
</Typography>
<Divider orientation="horizontal" inverted={true} />
</div>
<Button hierarchy="primary" ghost={true} onAction={selectFolder}>
Select folder
</Button>
</div>
);
};
export const Onboarding: Component<RouteSectionProps> = (props) => {
const navigate = useNavigate();
const activeURI = activeClanURI();
if (activeURI) {
// the user has already selected a clan, so we should navigate to it
console.log("active clan detected, navigating to it", activeURI);
navigateToClan(navigate, activeURI);
}
const [state, setState] = createSignal<State>("welcome");
// used to display an error in the welcome screen in the event of a failed
// clan creation
const [welcomeError, setWelcomeError] = createSignal<string | undefined>();
//
const [setupForm, { Form, Field }] = createForm<SetupForm>({
validate: valiForm(SetupSchema),
});
const formError = () => {
const formErrors = getErrors(setupForm);
return (
formErrors.name ||
formErrors.description ||
formErrors.directory ||
undefined
);
};
const onSelectFile = async () => {
const req = callApi("get_system_file", {
file_request: {
mode: "select_folder",
title: "Select a folder for you new Clan",
},
});
const resp = await req.result;
if (resp.status === "error") {
// just throw the first error, I can't imagine why there would be multiple
// errors for this call
throw new Error(resp.errors[0].message);
}
if (resp.status === "success" && resp.data) {
return resp.data[0];
}
throw new Error("No data returned from api call");
};
const onSubmit: SubmitHandler<SetupForm> = async (
{ name, description, directory },
event,
) => {
const path = `${directory}/${name}`;
const req = callApi("create_clan", {
opts: {
dest: path,
// todo allow users to select a template
template: "minimal",
initial: {
meta: {
name: name,
description: description,
// todo it tries to 'delete' icon if it's not provided
// this logic is unexpected, and needs reviewed.
icon: null,
},
machines: {},
instances: {},
services: {},
},
},
});
setState("creating");
const resp = await req.result;
if (resp.status === "error") {
setWelcomeError(resp.errors[0].message);
setState("welcome");
return;
}
if (resp.status === "success") {
addClanURI(path);
setActiveClanURI(path);
navigateToClan(navigate, path);
}
};
return (
<main id="welcome">
{background({ form: setupForm, state: state() })}
<div class="container">
<Switch>
<Match when={state() === "welcome"}>
{welcome({
setState,
welcomeError,
setWelcomeError,
})}
</Match>
<Match when={state() === "setup"}>
<div class="setup">
<div class="header">
<Button
hierarchy="secondary"
ghost={true}
icon="ArrowLeft"
onClick={() => setState("welcome")}
/>
<Typography hierarchy="headline" size="default" weight="bold">
Setup
</Typography>
</div>
<Form onSubmit={onSubmit}>
{formError() && (
<Alert
type="error"
icon="Info"
title="Form error"
description={formError() || ""}
/>
)}
<Fieldset name="meta">
<Field name="name">
{(field, input) => (
<TextInput
{...field}
label="Name"
value={field.value}
required
orientation="horizontal"
validationState={
getError(setupForm, "name") ? "invalid" : "valid"
}
input={{
...input,
placeholder: "Name your Clan",
}}
/>
)}
</Field>
<Divider inverted={true} />
<Field name="description">
{(field, input) => (
<TextArea
{...field}
value={field.value}
label="Description"
required
orientation="horizontal"
validationState={
getError(setupForm, "description")
? "invalid"
: "valid"
}
input={input}
/>
)}
</Field>
</Fieldset>
<Fieldset name="location">
<Field name="directory">
{(field, input) => (
<HostFileInput
onSelectFile={onSelectFile}
{...field}
value={field.value}
label="Select directory"
orientation="horizontal"
required={true}
validationState={
getError(setupForm, "directory") ? "invalid" : "valid"
}
input={input}
/>
)}
</Field>
</Fieldset>
<div class="form-controls">
<Button
type="submit"
hierarchy="primary"
endIcon="ArrowRight"
>
Next
</Button>
</div>
</Form>
</div>
</Match>
<Match when={state() === "creating"}>
<Creating />
</Match>
</Switch>
</div>
</main>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

@@ -0,0 +1,48 @@
import type { RouteDefinition } from "@solidjs/router/dist/types";
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
import { Clan } from "@/src/routes/Clan/Clan";
export const Routes: RouteDefinition[] = [
{
path: "/",
component: Onboarding,
},
{
path: "/clans",
children: [
{
path: "/",
component: () => (
<h1>
Clans (index) - (Doesnt really exist, just to keep the scene
mounted)
</h1>
),
},
{
path: "/:clanURI",
children: [
{
path: "/",
component: Clan,
},
{
path: "/machines",
children: [
{
path: "/",
component: () => <h1>Machines (Index)</h1>,
},
{
path: "/:machineID",
component: (props) => (
<h1>Machine ID: {props.params.machineID}</h1>
),
},
],
},
],
},
],
},
];

View File

@@ -0,0 +1,15 @@
.cubes-scene-container {
width: 100%;
height: 100vh;
cursor: pointer;
}
.toolbar-container {
position: absolute;
bottom: 10%;
width: 100%;
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,15 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { CubeScene } from "./cubes";
const meta: Meta = {
title: "scene/cubes",
component: CubeScene,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {},
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
#splash {
position: fixed;
inset: 0;
background: linear-gradient(to top, #e3e7e7, #edf1f1);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
pointer-events: none;
}
#splash .content {
display: flex;
flex-direction: column;
align-items: center;
}
.title {
@apply h-8 mb-8;
}
.loader {
@apply h-3 w-60 mb-3;
width: 18rem;
background: repeating-linear-gradient(
-45deg,
#bfd0d2 0px,
#bfd0d2 10px,
#f7f9fa 10px,
#f7f9fa 20px
);
animation: stripe-move 1s linear infinite;
background-size: 28px 28px; /* Sqrt(20^2 + 20^2) ~= 28 */
@apply border-2 border-solid rounded-[3px] border-bg-def-1;
}
@keyframes stripe-move {
0% {
background-position: 0 0;
}
100% {
background-position: 28px 0;
}
}

View File

@@ -0,0 +1,15 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Splash } from "./splash";
const meta: Meta = {
title: "scene/splash",
component: Splash,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {},
};

View File

@@ -0,0 +1,18 @@
import Logo from "@/logos/darknet-builder-logo.svg";
import "./splash.css";
import { Typography } from "../components/Typography/Typography";
export const Splash = () => (
<div id="splash">
<div class="content">
<span class="title">
<Logo />
</span>
<div class="loader"></div>
<Typography hierarchy="label" size="xs" weight="medium">
Loading new Clan
</Typography>
</div>
</div>
);

View File

@@ -0,0 +1,95 @@
import { createStore, produce } from "solid-js/store";
import { makePersisted } from "@solid-primitives/storage";
export type SceneData = Record<string, { position: [number, number] }>;
export interface ClanStoreType {
clanURIs: string[];
activeClanURI?: string;
sceneData: Record<string, SceneData>;
}
const [store, setStore] = makePersisted(
createStore<ClanStoreType>({
clanURIs: [],
sceneData: {},
}),
{
name: "clanStore",
storage: localStorage,
},
);
/**
* Retrieves the active clan URI from the store.
*
* @function
* @returns {string} The URI of the active clan.
*/
const activeClanURI = () => store.activeClanURI;
/**
* Updates the active Clan URI in the store.
*
* @param {string} uri - The URI to be set as the active Clan URI.
*/
const setActiveClanURI = (uri: string) => setStore("activeClanURI", uri);
/**
* Retrieves the current list of clan URIs from the store.
*
* @function clanURIs
* @returns {*} The clan URIs from the store.
*/
const clanURIs = (): string[] => store.clanURIs;
/**
* Adds a new clan URI to the list of clan URIs in the store.
*
* @param {string} uri - The URI of the clan to be added.
*
*/
const addClanURI = (uri: string) => {
setStore("clanURIs", store.clanURIs.length, uri);
setStore("sceneData", uri, {}); // Initialize empty scene data for every new clan URI
};
/**
* Removes a specified URI from the clan URI list and updates the active clan URI.
*
* This function modifies the store in the following ways:
* - Removes the specified URI from the `clanURIs` array.
* - Clears the `activeClanURI` if the removed URI matches the currently active URI.
* - Sets a new active clan URI to the last URI in the `clanURIs` array if the active clan URI is undefined
* and there are remaining clan URIs in the list.
*
* @param {string} uri - The URI to be removed from the clan list.
*/
const removeClanURI = (uri: string) => {
setStore(
produce((state) => {
// remove from the clan list
state.clanURIs = state.clanURIs.filter((el) => el !== uri);
// clear active clan uri if it's the one being removed
if (state.activeClanURI === uri) {
state.activeClanURI = undefined;
}
// select a new active URI if at least one remains
if (!state.activeClanURI && state.clanURIs.length > 0) {
state.activeClanURI = state.clanURIs[state.clanURIs.length - 1];
}
}),
);
};
export {
store,
setStore,
activeClanURI,
setActiveClanURI,
clanURIs,
addClanURI,
removeClanURI,
};

View File

@@ -11,6 +11,9 @@ const primaries = {
off: {
white: toRGB("#ffffff"),
black: toRGB("#000000"),
darknet_name: toRGB("#00ff57"),
darknet_label: toRGB("#2cff74"),
toolbar_border: toRGB("#2e4a4b"),
},
primary: {
50: toRGB("#f4f9f9"),

View File

@@ -1,6 +1,18 @@
{ pkgs, ... }:
{
gtk4,
webkitgtk_6_0,
lib,
clangStdenv,
fetchFromGitea,
gnumake,
cmake,
clang-tools,
pkg-config,
stdenv,
...
}:
pkgs.clangStdenv.mkDerivation {
clangStdenv.mkDerivation {
pname = "webview";
version = "nightly";
@@ -8,7 +20,7 @@ pkgs.clangStdenv.mkDerivation {
# We disallow remote connections from the UI on Linux
# TODO: Disallow remote connections on MacOS
src = pkgs.fetchFromGitea {
src = fetchFromGitea {
domain = "git.clan.lol";
owner = "clan";
repo = "webview";
@@ -37,23 +49,19 @@ pkgs.clangStdenv.mkDerivation {
];
# Dependencies used during the build process, if any
nativeBuildInputs = with pkgs; [
nativeBuildInputs = [
gnumake
cmake
clang-tools
pkg-config
];
buildInputs =
with pkgs;
[
]
++ pkgs.lib.optionals stdenv.isLinux [
webkitgtk_6_0
gtk4
];
buildInputs = lib.optionals stdenv.isLinux [
webkitgtk_6_0
gtk4
];
meta = with pkgs.lib; {
meta = with lib; {
description = "Tiny cross-platform webview library for C/C++. Uses WebKit (GTK/Cocoa) and Edge WebView2 (Windows)";
homepage = "https://github.com/webview/webview";
license = licenses.mit;

View File

@@ -2,7 +2,7 @@ import argparse
import logging
from clan_lib.backups.create import create_backup
from clan_lib.errors import ClanError
from clan_lib.flake import require_flake
from clan_lib.machines.machines import Machine
from clan_cli.completions import (
@@ -15,10 +15,8 @@ log = logging.getLogger(__name__)
def create_command(args: argparse.Namespace) -> None:
if args.flake is None:
msg = "Could not find clan flake toplevel directory"
raise ClanError(msg)
machine = Machine(name=args.machine, flake=args.flake)
flake = require_flake(args.flake)
machine = Machine(name=args.machine, flake=flake)
create_backup(machine=machine, provider=args.provider)

View File

@@ -0,0 +1,15 @@
from pathlib import Path
import pytest
from clan_lib.errors import ClanError
from clan_cli.tests.helpers import cli
def test_create_command_no_flake(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
with pytest.raises(ClanError):
cli.run(["backups", "create", "machine1"])

View File

@@ -1,7 +1,7 @@
import argparse
from clan_lib.backups.list import list_backups
from clan_lib.errors import ClanError
from clan_lib.flake import require_flake
from clan_lib.machines.machines import Machine
from clan_cli.completions import (
@@ -12,11 +12,8 @@ from clan_cli.completions import (
def list_command(args: argparse.Namespace) -> None:
if args.flake is None:
msg = "Could not find clan flake toplevel directory"
raise ClanError(msg)
machine = Machine(name=args.machine, flake=args.flake)
flake = require_flake(args.flake)
machine = Machine(name=args.machine, flake=flake)
backups = list_backups(machine=machine, provider=args.provider)
for backup in backups:
print(backup.name)

View File

@@ -0,0 +1,13 @@
from pathlib import Path
import pytest
from clan_lib.errors import ClanError
from clan_cli.tests.helpers import cli
def test_list_command_no_flake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
with pytest.raises(ClanError):
cli.run(["backups", "list", "machine1"])

View File

@@ -1,7 +1,7 @@
import argparse
from clan_lib.backups.restore import restore_backup
from clan_lib.errors import ClanError
from clan_lib.flake import require_flake
from clan_lib.machines.machines import Machine
from clan_cli.completions import (
@@ -12,10 +12,8 @@ from clan_cli.completions import (
def restore_command(args: argparse.Namespace) -> None:
if args.flake is None:
msg = "Could not find clan flake toplevel directory"
raise ClanError(msg)
machine = Machine(name=args.machine, flake=args.flake)
flake = require_flake(args.flake)
machine = Machine(name=args.machine, flake=flake)
restore_backup(
machine=machine,
provider=args.provider,

View File

@@ -0,0 +1,15 @@
from pathlib import Path
import pytest
from clan_lib.errors import ClanError
from clan_cli.tests.helpers import cli
def test_restore_command_no_flake(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
with pytest.raises(ClanError):
cli.run(["backups", "restore", "machine1", "provider1", "backup1"])

View File

@@ -2,13 +2,13 @@ import argparse
import logging
from clan_lib.clan.get import get_clan_details
from clan_lib.flake import Flake
from clan_lib.flake import require_flake
log = logging.getLogger(__name__)
def show_command(args: argparse.Namespace) -> None:
flake: Flake = args.flake
flake = require_flake(args.flake)
meta = get_clan_details(flake)
print(f"Name: {meta.get('name')}")

View File

@@ -1,4 +1,7 @@
from pathlib import Path
import pytest
from clan_lib.errors import ClanError
from clan_cli.tests.fixtures_flakes import FlakeForTest
from clan_cli.tests.helpers import cli
@@ -14,3 +17,19 @@ def test_clan_show(
assert "Name:" in output.out
assert "Name: test_flake_with_core" in output.out
assert "Description:" in output.out
def test_clan_show_no_flake(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capture_output: CaptureOutput
) -> None:
monkeypatch.chdir(tmp_path)
with pytest.raises(ClanError) as exc_info:
cli.run(["show"])
assert "No clan flake found in the current directory or its parents" in str(
exc_info.value
)
assert "Use the --flake flag to specify a clan flake path or URL" in str(
exc_info.value
)

View File

@@ -25,6 +25,7 @@ from .facts import cli as facts
from .flash import cli as flash_cli
from .hyperlink import help_hyperlink
from .machines import cli as machines
from .network import cli as network_cli
from .profiler import profile
from .ssh import deploy_info as ssh_cli
from .vars import cli as vars_cli
@@ -250,12 +251,12 @@ This subcommand allows seamless ssh access to the nixos-image builders or a mach
Examples:
$ clan ssh [ssh_args ...] berlin`
$ clan ssh berlin
Will ssh in to the machine called `berlin`, using the
`clan.core.networking.targetHost` specified in its configuration
$ clan ssh [ssh_args ...] --json [JSON]
$ clan ssh --json [JSON] --host-key-check none
Will ssh in to the machine based on the deployment information contained in
the json string. [JSON] can either be a json formatted string itself, or point
towards a file containing the deployment information
@@ -297,6 +298,8 @@ For more detailed information, visit: {help_hyperlink("secrets", "https://docs.c
description="Manage facts",
epilog=(
f"""
Note: Facts are being deprecated, please use Vars instead.
For a migration guide visit: {help_hyperlink("vars", "https://docs.clan.lol/guides/migrations/migration-facts-vars")}
This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate.
@@ -426,6 +429,26 @@ Examples:
)
select.register_parser(parser_select)
parser_network = subparsers.add_parser(
"network",
aliases=["net"],
# TODO: Add help="Manage networks" when network code is ready
# help="Manage networks",
description="Manage networks",
epilog=(
"""
show information about configured networks
Examples:
$ clan network list
Will list networks
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
network_cli.register_parser(parser_network)
parser_state = subparsers.add_parser(
"state",
aliases=["st"],
@@ -460,7 +483,7 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https:
state.register_parser(parser_state)
if argcomplete:
argcomplete.autocomplete(parser, exclude=["morph"])
argcomplete.autocomplete(parser, exclude=["morph", "network", "net"])
register_common_flags(parser)

View File

@@ -1,6 +1,7 @@
import argparse
import logging
from clan_lib.flake import require_flake
from clan_lib.machines.machines import Machine
from clan_cli.completions import add_dynamic_completer, complete_machines
@@ -37,9 +38,10 @@ def check_secrets(machine: Machine, service: None | str = None) -> bool:
def check_command(args: argparse.Namespace) -> None:
flake = require_flake(args.flake)
machine = Machine(
name=args.machine,
flake=args.flake,
flake=flake,
)
check_secrets(machine, service=args.service)

View File

@@ -0,0 +1,15 @@
from pathlib import Path
import pytest
from clan_lib.errors import ClanError
from clan_cli.tests.helpers import cli
def test_check_command_no_flake(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
with pytest.raises(ClanError):
cli.run(["facts", "check", "machine1"])

View File

@@ -9,6 +9,7 @@ from tempfile import TemporaryDirectory
from clan_lib.cmd import RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.flake import require_flake
from clan_lib.git import commit_files
from clan_lib.machines.list import list_full_machines
from clan_lib.machines.machines import Machine
@@ -223,11 +224,8 @@ def generate_facts(
def generate_command(args: argparse.Namespace) -> None:
if args.flake is None:
msg = "Could not find clan flake toplevel directory"
raise ClanError(msg)
machines: list[Machine] = list(list_full_machines(args.flake).values())
flake = require_flake(args.flake)
machines: list[Machine] = list(list_full_machines(flake).values())
if len(args.machines) > 0:
machines = list(
filter(

Some files were not shown because too many files have changed in this diff Show More