commit 8254a28baa00845c84e128e82388a3e316fb9bd1 Author: Gregor Lohaus Date: Wed Apr 8 04:29:35 2026 +0200 init diff --git a/.devenv.flake.nix b/.devenv.flake.nix new file mode 100644 index 0000000..38ae300 --- /dev/null +++ b/.devenv.flake.nix @@ -0,0 +1,513 @@ +{ + inputs = + let + vars = { + version = "1.11.2"; + system = "x86_64-linux"; + devenv_root = "/home/anon/Dev/glstack"; + project_input_ref = "path:/home/anon/Dev/glstack"; + devenv_dotfile = "/home/anon/Dev/glstack/.devenv"; + devenv_dotfile_path = ./.devenv; + devenv_tmpdir = "/run/user/1000"; + devenv_runtime = "/run/user/1000/devenv-5149e7a"; + devenv_istesting = false; + devenv_direnvrc_latest_version = 1; + container_name = null; + active_profiles = [ + ]; + hostname = "debian"; + username = "anon"; + git_root = null; + secretspec = null; +}; + in + { + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + pre-commit-hooks.follows = "git-hooks"; + nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; + devenv.url = "github:cachix/devenv?dir=src/modules"; + } + // ( + if builtins.pathExists (vars.devenv_dotfile_path + "/flake.json") then + builtins.fromJSON (builtins.readFile (vars.devenv_dotfile_path + "/flake.json")) + else + { } + ); + + outputs = + { nixpkgs, ... }@inputs: + let + vars = { + version = "1.11.2"; + system = "x86_64-linux"; + devenv_root = "/home/anon/Dev/glstack"; + project_input_ref = "path:/home/anon/Dev/glstack"; + devenv_dotfile = "/home/anon/Dev/glstack/.devenv"; + devenv_dotfile_path = ./.devenv; + devenv_tmpdir = "/run/user/1000"; + devenv_runtime = "/run/user/1000/devenv-5149e7a"; + devenv_istesting = false; + devenv_direnvrc_latest_version = 1; + container_name = null; + active_profiles = [ + ]; + hostname = "debian"; + username = "anon"; + git_root = null; + secretspec = null; +}; + devenv = + if builtins.pathExists (vars.devenv_dotfile_path + "/devenv.json") then + builtins.fromJSON (builtins.readFile (vars.devenv_dotfile_path + "/devenv.json")) + else + { }; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + # Function to create devenv configuration for a specific system with profiles support + mkDevenvForSystem = + targetSystem: + let + getOverlays = + inputName: inputAttrs: + map ( + overlay: + let + input = + inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays."); + in + input.overlays.${overlay} + or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}") + ) inputAttrs.overlays or [ ]; + overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or { })); + permittedUnfreePackages = + devenv.nixpkgs.per-platform."${targetSystem}".permittedUnfreePackages + or devenv.nixpkgs.permittedUnfreePackages or [ ]; + pkgs = import nixpkgs { + system = targetSystem; + config = { + allowUnfree = + devenv.nixpkgs.per-platform."${targetSystem}".allowUnfree or devenv.nixpkgs.allowUnfree + or devenv.allowUnfree or false; + allowBroken = + devenv.nixpkgs.per-platform."${targetSystem}".allowBroken or devenv.nixpkgs.allowBroken + or devenv.allowBroken or false; + cudaSupport = + devenv.nixpkgs.per-platform."${targetSystem}".cudaSupport or devenv.nixpkgs.cudaSupport or false; + cudaCapabilities = + devenv.nixpkgs.per-platform."${targetSystem}".cudaCapabilities or devenv.nixpkgs.cudaCapabilities + or [ ]; + permittedInsecurePackages = + devenv.nixpkgs.per-platform."${targetSystem}".permittedInsecurePackages + or devenv.nixpkgs.permittedInsecurePackages or devenv.permittedInsecurePackages or [ ]; + allowUnfreePredicate = + if (permittedUnfreePackages != [ ]) then + (pkg: builtins.elem (nixpkgs.lib.getName pkg) permittedUnfreePackages) + else + (_: false); + }; + inherit overlays; + }; + inherit (pkgs) lib; + importModule = + path: + if lib.hasPrefix "./" path then + if lib.hasSuffix ".nix" path then + ./. + (builtins.substring 1 255 path) + else + ./. + (builtins.substring 1 255 path) + "/devenv.nix" + else if lib.hasPrefix "../" path then + # For parent directory paths, concatenate with /. + # ./. refers to the directory containing this file (project root) + # So ./. + "/../shared" = /../shared + if lib.hasSuffix ".nix" path then ./. + "/${path}" else ./. + "/${path}/devenv.nix" + else + let + paths = lib.splitString "/" path; + name = builtins.head paths; + input = inputs.${name} or (throw "Unknown input ${name}"); + subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}"; + devenvpath = "${input}" + subpath; + devenvdefaultpath = devenvpath + "/devenv.nix"; + in + if lib.hasSuffix ".nix" devenvpath then + devenvpath + else if builtins.pathExists devenvdefaultpath then + devenvdefaultpath + else + throw (devenvdefaultpath + " file does not exist for input ${name}."); + + # Phase 1: Base evaluation to extract profile definitions + baseProject = pkgs.lib.evalModules { + specialArgs = inputs // { + inherit inputs; + }; + modules = [ + ( + { config, ... }: + { + _module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]); + } + ) + (inputs.devenv.modules + /top-level.nix) + ( + { options, ... }: + { + config.devenv = lib.mkMerge [ + { + cliVersion = vars.version; + root = vars.devenv_root; + dotfile = vars.devenv_dotfile; + } + (pkgs.lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) { + tmpdir = vars.devenv_tmpdir; + }) + (pkgs.lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) { + isTesting = vars.devenv_istesting; + }) + (pkgs.lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) { + runtime = vars.devenv_runtime; + }) + (pkgs.lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) { + direnvrcLatestVersion = vars.devenv_direnvrc_latest_version; + }) + ]; + } + ) + ( + { options, ... }: + { + config = lib.mkMerge [ + (pkgs.lib.optionalAttrs (builtins.hasAttr "git" options) { + git.root = vars.git_root; + }) + ]; + } + ) + (pkgs.lib.optionalAttrs (vars.container_name != null) { + container.isBuilding = pkgs.lib.mkForce true; + containers.${vars.container_name}.isBuilding = true; + }) + ] + ++ (map importModule (devenv.imports or [ ])) + ++ [ + (if builtins.pathExists ./devenv.nix then ./devenv.nix else { }) + (devenv.devenv or { }) + (if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { }) + ( + if builtins.pathExists (vars.devenv_dotfile_path + "/cli-options.nix") then + import (vars.devenv_dotfile_path + "/cli-options.nix") + else + { } + ) + ]; + }; + + # Phase 2: Extract and apply profiles using extendModules with priority overrides + project = + let + # Build ordered list of profile names: hostname -> user -> manual + manualProfiles = vars.active_profiles; + currentHostname = vars.hostname; + currentUsername = vars.username; + hostnameProfiles = lib.optional ( + currentHostname != "" + && builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { }) + ) "hostname.${currentHostname}"; + userProfiles = lib.optional ( + currentUsername != "" && builtins.hasAttr currentUsername (baseProject.config.profiles.user or { }) + ) "user.${currentUsername}"; + + # Ordered list of profiles to activate + orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles; + + # Resolve profile extends with cycle detection + resolveProfileExtends = + profileName: visited: + if builtins.elem profileName visited then + throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}" + else + let + profile = getProfileConfig profileName; + extends = profile.extends or [ ]; + newVisited = visited ++ [ profileName ]; + extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends); + in + extendedProfiles ++ [ profileName ]; + + # Get profile configuration by name from baseProject + getProfileConfig = + profileName: + if lib.hasPrefix "hostname." profileName then + let + name = lib.removePrefix "hostname." profileName; + in + baseProject.config.profiles.hostname.${name} + else if lib.hasPrefix "user." profileName then + let + name = lib.removePrefix "user." profileName; + in + baseProject.config.profiles.user.${name} + else + let + availableProfiles = builtins.attrNames (baseProject.config.profiles or { }); + hostnameProfiles = map (n: "hostname.${n}") ( + builtins.attrNames (baseProject.config.profiles.hostname or { }) + ); + userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { })); + allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles; + in + baseProject.config.profiles.${profileName} + or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}"); + + # Fold over ordered profiles to build final list with extends + expandedProfiles = lib.foldl' ( + acc: profileName: + let + allProfileNames = resolveProfileExtends profileName [ ]; + in + acc ++ allProfileNames + ) [ ] orderedProfiles; + + # Map over expanded profiles and apply priorities + allPrioritizedModules = lib.imap0 ( + index: profileName: + let + # Decrement priority for each profile (lower = higher precedence) + # Start with the next lowest priority after the default priority for values (100) + profilePriority = (lib.modules.defaultOverridePriority - 1) - index; + profileConfig = getProfileConfig profileName; + + # Check if an option type needs explicit override to resolve conflicts + # Only apply overrides to LEAF values (scalars), not collection types that can merge + typeNeedsOverride = + type: + if type == null then + false + else + let + typeName = type.name or type._type or ""; + + # True leaf types that need priority resolution when they conflict + isLeafType = builtins.elem typeName [ + "str" + "int" + "bool" + "enum" + "path" + "package" + "float" + "anything" + ]; + in + if isLeafType then + true + else if typeName == "nullOr" then + # For nullOr, check the wrapped type recursively + let + innerType = + type.elemType + or (if type ? nestedTypes && type.nestedTypes ? elemType then type.nestedTypes.elemType else null); + in + if innerType != null then typeNeedsOverride innerType else false + else + # Everything else (collections, submodules, etc.) should merge naturally + false; + + # Check if a config path needs explicit override + pathNeedsOverride = + optionPath: + let + # Try direct option first + directOption = lib.attrByPath optionPath null baseProject.options; + in + if directOption != null && lib.isOption directOption then + typeNeedsOverride directOption.type + else if optionPath != [ ] then + # Check parent for freeform type + let + parentPath = lib.init optionPath; + parentOption = lib.attrByPath parentPath null baseProject.options; + in + if parentOption != null && lib.isOption parentOption then + let + # Look for freeform type: + # 1. Standard location: type.freeformType (primary) + # 2. Nested location: type.nestedTypes.freeformType (evaluated form) + freeformType = parentOption.type.freeformType or parentOption.type.nestedTypes.freeformType or null; + elementType = + if freeformType ? elemType then + freeformType.elemType + else if freeformType ? nestedTypes && freeformType.nestedTypes ? elemType then + freeformType.nestedTypes.elemType + else + freeformType; + in + typeNeedsOverride elementType + else + false + else + false; + + # Support overriding both plain attrset modules and functions + applyModuleOverride = + config: + if builtins.isFunction config then + let + wrapper = args: applyOverrideRecursive (config args) [ ]; + in + lib.mirrorFunctionArgs config wrapper + else + applyOverrideRecursive config [ ]; + + # Apply overrides recursively based on option types + applyOverrideRecursive = + config: optionPath: + if lib.isAttrs config && config ? _type then + config # Don't touch values with existing type metadata + else if lib.isAttrs config then + lib.mapAttrs (name: value: applyOverrideRecursive value (optionPath ++ [ name ])) config + else if pathNeedsOverride optionPath then + lib.mkOverride profilePriority config + else + config; + + # Apply priority overrides recursively to the deferredModule imports structure + prioritizedConfig = ( + profileConfig.module + // { + imports = lib.map ( + importItem: + importItem + // { + imports = lib.map (nestedImport: applyModuleOverride nestedImport) (importItem.imports or [ ]); + } + ) (profileConfig.module.imports or [ ]); + } + ); + in + prioritizedConfig + ) expandedProfiles; + in + if allPrioritizedModules == [ ] then + baseProject + else + baseProject.extendModules { modules = allPrioritizedModules; }; + + config = project.config; + + options = pkgs.nixosOptionsDoc { + options = builtins.removeAttrs project.options [ "_module" ]; + warningsAreErrors = false; + # Unpack Nix types, e.g. literalExpression, mDoc. + transformOptions = + let + isDocType = + v: + builtins.elem v [ + "literalDocBook" + "literalExpression" + "literalMD" + "mdDoc" + ]; + in + lib.attrsets.mapAttrs ( + _: v: + if v ? _type && isDocType v._type then + v.text + else if v ? _type && v._type == "derivation" then + v.name + else + v + ); + }; + + # Recursively search for outputs in the config. + # This is used when not building a specific output by attrpath. + build = + options: config: + lib.concatMapAttrs ( + name: option: + if lib.isOption option then + let + typeName = option.type.name or ""; + in + if + builtins.elem typeName [ + "output" + "outputOf" + ] + then + { ${name} = config.${name}; } + else + { } + else if builtins.isAttrs option && !lib.isDerivation option then + let + v = build option config.${name}; + in + if v != { } then + { + ${name} = v; + } + else + { } + else + { } + ) options; + in + { + inherit + config + options + build + project + ; + shell = config.shell; + packages = { + optionsJSON = options.optionsJSON; + # deprecated + inherit (config) + info + procfileScript + procfileEnv + procfile + ; + ci = config.ciDerivation; + }; + }; + + # Generate per-system devenv configurations + perSystem = nixpkgs.lib.genAttrs systems mkDevenvForSystem; + + # Default devenv for the current system + currentSystemDevenv = perSystem.${vars.system}; + in + { + devShell = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.shell); + packages = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.packages); + + # Per-system devenv configurations + devenv = { + # Default devenv for the current system + inherit (currentSystemDevenv) + config + options + build + shell + packages + project + ; + # Per-system devenv configurations + inherit perSystem; + }; + + # Legacy build output + build = currentSystemDevenv.build currentSystemDevenv.options currentSystemDevenv.config; + }; +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64650e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +.devenv +devenv.lock diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 0000000..1813968 --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,4 @@ +[[language]] +name="typescript" +roots = ["package.json"] +language-servers = ["typescript-language-server"] diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..2c78c9a --- /dev/null +++ b/.ignore @@ -0,0 +1,2 @@ +node_modules +.devenv diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..764c1dd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f2f951 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# glstack + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..03321de --- /dev/null +++ b/bun.lock @@ -0,0 +1,47 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "glstack", + "dependencies": { + "@clack/prompts": "^1.2.0", + "@gregorlohaus/tdir": "^0.1.1", + "zod": "^4.3.6", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], + + "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], + + "@gregorlohaus/tdir": ["@gregorlohaus/tdir@0.1.1", "", { "peerDependencies": { "zod": "^4" } }, "sha512-4NlHif5Pn6Vh1TzCj8B1d+pz8ab5/CodC2Cq9HVr1wHdFlgXM/yjtZEDLdBMtwqz1n1oCCEjzV7XYbin2ywsjQ=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], + + "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + } +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..ed38107 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,7 @@ +{ pkgs, lib, config, inputs, ... }: + +{ + packages = [ pkgs.bun pkgs.nodejs_24]; + languages.typescript.enable = true; + +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f562a76 --- /dev/null +++ b/index.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env bun + +import * as p from "@clack/prompts"; +import { initRenderer } from '@gregorlohaus/tdir' +import { z } from 'zod' +import join from "node:path"; + +p.intro("create-glstack"); + +const project = await p.group( + { + name: () => + p.text({ + message: "What would you like to name your project?", + placeholder: "glstack-test", + defaultValue: "glstack-test", + validate: (value) => { + return undefined + }, + }), + goprefix: () => + p.text({ + message: "What would you like to use as a go package prefix?", + placeholder: "github.com/glstack-test", + defaultValue: "github.com/glstack-test", + validate: (value) => { + return undefined + }, + }), + }, + { + onCancel: () => { + p.cancel("Operation cancelled."); + process.exit(0); + }, + } +); +const createRenderer = initRenderer('./template') +const render = createRenderer(z.object({ + project: z.object({ + name: z.string(), + goprefix: z.string() + }) +})) +const destDir = join.join("./",project.name); +render(destDir,{project}) +// TODO: template rendering — copy/generate files into destDir + +const s = p.spinner(); +s.start("Installing dependencies"); +await Bun.$`bun install`.cwd(join.join(destDir)).quiet(); +await Bun.$`bun install`.cwd(join.join(destDir,'packages','rpc')).quiet(); +await Bun.$`bun install`.cwd(join.join(destDir,'apps','web')).quiet(); +s.stop("Dependencies installed."); + +p.outro("You're all set!"); diff --git a/package.json b/package.json new file mode 100644 index 0000000..280a5ff --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "create-glstack", + "version": "0.0.1", + "module": "index.ts", + "type": "module", + "bin": { + "create-glstack": "./index.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@clack/prompts": "^1.2.0", + "@gregorlohaus/tdir": "^0.1.1", + "zod": "^4.3.6" + } +} diff --git a/template/.envrc b/template/.envrc new file mode 100644 index 0000000..cc5c18b --- /dev/null +++ b/template/.envrc @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence all output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..f58ea61 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,12 @@ +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml +node_modules +scripts/node_modules diff --git a/template/.ignore b/template/.ignore new file mode 100644 index 0000000..ae49879 --- /dev/null +++ b/template/.ignore @@ -0,0 +1,10 @@ +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/template/apps/web/.gitignore b/template/apps/web/.gitignore new file mode 100644 index 0000000..cd8e1d7 --- /dev/null +++ b/template/apps/web/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +# Paraglide +src/lib/paraglide +project.inlang/cache/ diff --git a/template/apps/web/.ignore b/template/apps/web/.ignore new file mode 100644 index 0000000..cd8e1d7 --- /dev/null +++ b/template/apps/web/.ignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +# Paraglide +src/lib/paraglide +project.inlang/cache/ diff --git a/template/apps/web/.npmrc b/template/apps/web/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/template/apps/web/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/template/apps/web/.prettierignore b/template/apps/web/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/template/apps/web/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/template/apps/web/.prettierrc b/template/apps/web/.prettierrc new file mode 100644 index 0000000..819fa57 --- /dev/null +++ b/template/apps/web/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/routes/layout.css" +} diff --git a/template/apps/web/.vscode/extensions.json b/template/apps/web/.vscode/extensions.json new file mode 100644 index 0000000..252fc41 --- /dev/null +++ b/template/apps/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"] +} diff --git a/template/apps/web/.vscode/settings.json b/template/apps/web/.vscode/settings.json new file mode 100644 index 0000000..bc31e15 --- /dev/null +++ b/template/apps/web/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/template/apps/web/README.md b/template/apps/web/README.md new file mode 100644 index 0000000..db53934 --- /dev/null +++ b/template/apps/web/README.md @@ -0,0 +1,42 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +bun x sv@0.14.0 create --template minimal --types ts --add prettier tailwindcss="plugins:typography" paraglide="languageTags:en, de-De+demo:no" --install bun web +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/template/apps/web/components.json b/template/apps/web/components.json new file mode 100644 index 0000000..0eaa3b2 --- /dev/null +++ b/template/apps/web/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/routes/layout.css", + "baseColor": "mist" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry", + "style": "lyra", + "iconLibrary": "lucide", + "menuColor": "default", + "menuAccent": "subtle" +} diff --git a/template/apps/web/messages/de-de.json b/template/apps/web/messages/de-de.json new file mode 100644 index 0000000..d713555 --- /dev/null +++ b/template/apps/web/messages/de-de.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from de-de!" +} diff --git a/template/apps/web/messages/en.json b/template/apps/web/messages/en.json new file mode 100644 index 0000000..37a9894 --- /dev/null +++ b/template/apps/web/messages/en.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from en!" +} diff --git a/template/apps/web/package.json b/template/apps/web/package.json new file mode 100644 index 0000000..9f8ab80 --- /dev/null +++ b/template/apps/web/package.json @@ -0,0 +1,59 @@ +{ + "name": "web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check .", + "format": "prettier --write ." + }, + "devDependencies": { + "@fontsource-variable/roboto": "^5.2.10", + "@inlang/paraglide-js": "^2.10.0", + "@internationalized/date": "^3.12.0", + "@lucide/svelte": "^0.577.0", + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/table-core": "^8.21.3", + "bits-ui": "^2.16.3", + "clsx": "^2.1.1", + "embla-carousel-svelte": "^8.6.0", + "formsnap": "^2.0.1", + "layerchart": "2.0.0-next.48", + "mode-watcher": "^1.1.0", + "paneforge": "^1.0.2", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "shadcn-svelte": "^1.2.7", + "svelte": "^5.54.0", + "svelte-check": "^4.4.2", + "svelte-sonner": "^1.1.0", + "sveltekit-superforms": "^2.30.0", + "tailwind-merge": "^3.5.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "vaul-svelte": "^1.0.0-next.7", + "vite": "^7.3.1" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.11.0", + "@connectrpc/connect": "^2.1.1", + "@connectrpc/connect-web": "^2.1.1", + "@<@var(context.project.name)>/rpc": "workspace:*", + "@tanstack/query-db-collection": "^1.0.33", + "@tanstack/svelte-db": "^0.1.79", + "@tanstack/svelte-query": "^6.1.13" + } +} diff --git a/template/apps/web/project.inlang/settings.json b/template/apps/web/project.inlang/settings.json new file mode 100644 index 0000000..e9eac7e --- /dev/null +++ b/template/apps/web/project.inlang/settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + }, + "baseLocale": "en", + "locales": ["en", "de-de"] +} diff --git a/template/apps/web/src/app.html b/template/apps/web/src/app.html new file mode 100644 index 0000000..639cbb8 --- /dev/null +++ b/template/apps/web/src/app.html @@ -0,0 +1,16 @@ + + + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/template/apps/web/src/hooks.server.ts b/template/apps/web/src/hooks.server.ts new file mode 100644 index 0000000..ac5e591 --- /dev/null +++ b/template/apps/web/src/hooks.server.ts @@ -0,0 +1,17 @@ +import type { Handle } from '@sveltejs/kit'; +import { getTextDirection } from '$lib/paraglide/runtime'; +import { paraglideMiddleware } from '$lib/paraglide/server'; + +const handleParaglide: Handle = ({ event, resolve }) => + paraglideMiddleware(event.request, ({ request, locale }) => { + event.request = request; + + return resolve(event, { + transformPageChunk: ({ html }) => + html + .replace('%paraglide.lang%', locale) + .replace('%paraglide.dir%', getTextDirection(locale)) + }); + }); + +export const handle: Handle = handleParaglide; diff --git a/template/apps/web/src/hooks.ts b/template/apps/web/src/hooks.ts new file mode 100644 index 0000000..392875d --- /dev/null +++ b/template/apps/web/src/hooks.ts @@ -0,0 +1,4 @@ +import type { Reroute } from '@sveltejs/kit'; +import { deLocalizeUrl } from '$lib/paraglide/runtime'; + +export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname; diff --git a/template/apps/web/src/lib/assets/favicon.svg b/template/apps/web/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/template/apps/web/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/template/apps/web/src/lib/components/todos/CreateTodo.svelte b/template/apps/web/src/lib/components/todos/CreateTodo.svelte new file mode 100644 index 0000000..1edb1ed --- /dev/null +++ b/template/apps/web/src/lib/components/todos/CreateTodo.svelte @@ -0,0 +1,26 @@ + + +
+ + + + Todo + + + + + +
diff --git a/template/apps/web/src/lib/components/todos/ListTodos.svelte b/template/apps/web/src/lib/components/todos/ListTodos.svelte new file mode 100644 index 0000000..a530e8f --- /dev/null +++ b/template/apps/web/src/lib/components/todos/ListTodos.svelte @@ -0,0 +1,50 @@ + + + + {#if browser} + {#each todos as todo (todo.id)} + + {/each} + {:else} + {#each initialTodos as todo} + + {/each} + {/if} + diff --git a/template/apps/web/src/lib/components/todos/Todo.svelte b/template/apps/web/src/lib/components/todos/Todo.svelte new file mode 100644 index 0000000..79e566c --- /dev/null +++ b/template/apps/web/src/lib/components/todos/Todo.svelte @@ -0,0 +1,70 @@ + + +update()}> + + + + + task + + + + + + + + done + + update()}> + + + +
+ + + Created + + + {(new Date(todoState.createdAt||"now")).toLocaleString()} + + + + + Updated + + + {(new Date(todoState.updatesAt||"now")).toLocaleString()} + + + +
+
+
+
+ diff --git a/template/apps/web/src/lib/components/ui/accordion/accordion-content.svelte b/template/apps/web/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000..24f1f41 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,27 @@ + + + +
+ {@render children?.()} +
+
diff --git a/template/apps/web/src/lib/components/ui/accordion/accordion-item.svelte b/template/apps/web/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000..9d3919d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/accordion/accordion-trigger.svelte b/template/apps/web/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000..383272d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + + {@render children?.()} + + + diff --git a/template/apps/web/src/lib/components/ui/accordion/accordion.svelte b/template/apps/web/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 0000000..b19a3c5 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,19 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/accordion/index.ts b/template/apps/web/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000..ac343a1 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,16 @@ +import Root from "./accordion.svelte"; +import Content from "./accordion-content.svelte"; +import Item from "./accordion-item.svelte"; +import Trigger from "./accordion-trigger.svelte"; + +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger, +}; diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..7e63004 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,27 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..a4ce03c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,27 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..710727d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..7b26f8a --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..4d133b7 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..d6df4d3 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte new file mode 100644 index 0000000..4db6d7e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..9ffcc85 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..f0a19a8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..5cb7e7f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000..b22d1d5 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog.svelte new file mode 100644 index 0000000..7ea78bb --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/alert-dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/alert-dialog/index.ts b/template/apps/web/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..ca81c2a --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,40 @@ +import Root from "./alert-dialog.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; +import Media from "./alert-dialog-media.svelte"; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Media, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, + Media as AlertDialogMedia, +}; diff --git a/template/apps/web/src/lib/components/ui/alert/alert-action.svelte b/template/apps/web/src/lib/components/ui/alert/alert-action.svelte new file mode 100644 index 0000000..0f90609 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert/alert-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/alert/alert-description.svelte b/template/apps/web/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..e62a009 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/alert/alert-title.svelte b/template/apps/web/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..3e339a3 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,23 @@ + + +
svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3", + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/alert/alert.svelte b/template/apps/web/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..b2eecfa --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,43 @@ + + + + + diff --git a/template/apps/web/src/lib/components/ui/alert/index.ts b/template/apps/web/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..071b113 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/alert/index.ts @@ -0,0 +1,17 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; +import Action from "./alert-action.svelte"; +export { alertVariants, type AlertVariant } from "./alert.svelte"; + +export { + Root, + Description, + Title, + Action, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, + Action as AlertAction, +}; diff --git a/template/apps/web/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte b/template/apps/web/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte new file mode 100644 index 0000000..815aab0 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/aspect-ratio/index.ts b/template/apps/web/src/lib/components/ui/aspect-ratio/index.ts new file mode 100644 index 0000000..985c75f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/aspect-ratio/index.ts @@ -0,0 +1,3 @@ +import Root from "./aspect-ratio.svelte"; + +export { Root, Root as AspectRatio }; diff --git a/template/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte b/template/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte new file mode 100644 index 0000000..ca9d9ed --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte @@ -0,0 +1,26 @@ + + +svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte b/template/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..63c8b02 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,20 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte b/template/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte new file mode 100644 index 0000000..536c308 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte @@ -0,0 +1,23 @@ + + +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/avatar/avatar-group.svelte b/template/apps/web/src/lib/components/ui/avatar/avatar-group.svelte new file mode 100644 index 0000000..78714fc --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/avatar-group.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/template/apps/web/src/lib/components/ui/avatar/avatar-image.svelte b/template/apps/web/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..f41cf8f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/avatar/avatar.svelte b/template/apps/web/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..ea6fde5 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,26 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/avatar/index.ts b/template/apps/web/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..38ccef8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,22 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; +import Badge from "./avatar-badge.svelte"; +import Group from "./avatar-group.svelte"; +import GroupCount from "./avatar-group-count.svelte"; + +export { + Root, + Image, + Fallback, + Badge, + Group, + GroupCount, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, + Badge as AvatarBadge, + Group as AvatarGroup, + GroupCount as AvatarGroupCount, +}; diff --git a/template/apps/web/src/lib/components/ui/badge/badge.svelte b/template/apps/web/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..2cfe06f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/badge/index.ts b/template/apps/web/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 0000000..9e1e22c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,23 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 0000000..e9c77ea --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 0000000..e6bc17d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,31 @@ + + +{#if child} + {@render child({ props: attrs })} +{:else} + + {@render children?.()} + +{/if} diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 0000000..44fd50b --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,20 @@ + + +
      + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte new file mode 100644 index 0000000..5fb6979 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 0000000..0f62fba --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,27 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb.svelte new file mode 100644 index 0000000..29ea3f5 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/breadcrumb.svelte @@ -0,0 +1,22 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/breadcrumb/index.ts b/template/apps/web/src/lib/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..dc914ec --- /dev/null +++ b/template/apps/web/src/lib/components/ui/breadcrumb/index.ts @@ -0,0 +1,25 @@ +import Root from "./breadcrumb.svelte"; +import Ellipsis from "./breadcrumb-ellipsis.svelte"; +import Item from "./breadcrumb-item.svelte"; +import Separator from "./breadcrumb-separator.svelte"; +import Link from "./breadcrumb-link.svelte"; +import List from "./breadcrumb-list.svelte"; +import Page from "./breadcrumb-page.svelte"; + +export { + Root, + Ellipsis, + Item, + Separator, + Link, + List, + Page, + // + Root as Breadcrumb, + Ellipsis as BreadcrumbEllipsis, + Item as BreadcrumbItem, + Separator as BreadcrumbSeparator, + Link as BreadcrumbLink, + List as BreadcrumbList, + Page as BreadcrumbPage, +}; diff --git a/template/apps/web/src/lib/components/ui/button-group/button-group-separator.svelte b/template/apps/web/src/lib/components/ui/button-group/button-group-separator.svelte new file mode 100644 index 0000000..a13f023 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/button-group/button-group-separator.svelte @@ -0,0 +1,23 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/button-group/button-group-text.svelte b/template/apps/web/src/lib/components/ui/button-group/button-group-text.svelte new file mode 100644 index 0000000..d5b607f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/button-group/button-group-text.svelte @@ -0,0 +1,28 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render mergedProps.children?.()} +
    +{/if} diff --git a/template/apps/web/src/lib/components/ui/button-group/button-group.svelte b/template/apps/web/src/lib/components/ui/button-group/button-group.svelte new file mode 100644 index 0000000..6137433 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/button-group/button-group.svelte @@ -0,0 +1,46 @@ + + + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/button-group/index.ts b/template/apps/web/src/lib/components/ui/button-group/index.ts new file mode 100644 index 0000000..664167f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/button-group/index.ts @@ -0,0 +1,15 @@ +import Root, { buttonGroupVariants, type ButtonGroupOrientation } from "./button-group.svelte"; +import Text from "./button-group-text.svelte"; +import Separator from "./button-group-separator.svelte"; + +export { + Root, + Text, + Separator, + buttonGroupVariants, + type ButtonGroupOrientation, + // + Root as ButtonGroup, + Text as ButtonGroupText, + Separator as ButtonGroupSeparator, +}; diff --git a/template/apps/web/src/lib/components/ui/button/button.svelte b/template/apps/web/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..b8fd3bc --- /dev/null +++ b/template/apps/web/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/template/apps/web/src/lib/components/ui/button/index.ts b/template/apps/web/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-caption.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-caption.svelte new file mode 100644 index 0000000..5c93037 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-caption.svelte @@ -0,0 +1,76 @@ + + +{#snippet MonthSelect()} + { + if (!placeholder) return; + const v = Number.parseInt(e.currentTarget.value); + const newPlaceholder = placeholder.set({ month: v }); + placeholder = newPlaceholder.subtract({ months: monthIndex }); + }} + /> +{/snippet} + +{#snippet YearSelect()} + +{/snippet} + +{#if captionLayout === "dropdown"} + {@render MonthSelect()} + {@render YearSelect()} +{:else if captionLayout === "dropdown-months"} + {@render MonthSelect()} + {#if placeholder} + {formatYear(placeholder)} + {/if} +{:else if captionLayout === "dropdown-years"} + {#if placeholder} + {formatMonth(placeholder)} + {/if} + {@render YearSelect()} +{:else} + {formatMonth(month)} {formatYear(month)} +{/if} diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-cell.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-cell.svelte new file mode 100644 index 0000000..7ceb5e1 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-day.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-day.svelte new file mode 100644 index 0000000..edc8c9d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-day.svelte @@ -0,0 +1,33 @@ + + +span]:text-xs [&>span]:opacity-70", + className + )} + {...restProps} +/> diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-grid-body.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-grid-body.svelte new file mode 100644 index 0000000..8cd86de --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-grid-body.svelte @@ -0,0 +1,12 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-grid-head.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-grid-head.svelte new file mode 100644 index 0000000..333edc4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-grid-head.svelte @@ -0,0 +1,12 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-grid-row.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-grid-row.svelte new file mode 100644 index 0000000..9032236 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-grid.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-grid.svelte new file mode 100644 index 0000000..5fbb18f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-grid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-head-cell.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-head-cell.svelte new file mode 100644 index 0000000..131807e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-head-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-header.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-header.svelte new file mode 100644 index 0000000..c39e955 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-header.svelte @@ -0,0 +1,19 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-heading.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-heading.svelte new file mode 100644 index 0000000..a9b9810 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-heading.svelte @@ -0,0 +1,16 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-month-select.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-month-select.svelte new file mode 100644 index 0000000..ed75b1d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-month-select.svelte @@ -0,0 +1,48 @@ + + + + + {#snippet child({ props, monthItems, selectedMonthItem })} + + + {/snippet} + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-month.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-month.svelte new file mode 100644 index 0000000..7bdafe7 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-month.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-months.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-months.svelte new file mode 100644 index 0000000..f717a9d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-months.svelte @@ -0,0 +1,19 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-nav.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-nav.svelte new file mode 100644 index 0000000..27f33d7 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-nav.svelte @@ -0,0 +1,19 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-next-button.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-next-button.svelte new file mode 100644 index 0000000..82dd739 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-next-button.svelte @@ -0,0 +1,36 @@ + + +{#snippet Fallback()} + +{/snippet} + + + {#if children} + {@render children?.()} + {:else} + {@render Fallback()} + {/if} + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-prev-button.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-prev-button.svelte new file mode 100644 index 0000000..383312e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-prev-button.svelte @@ -0,0 +1,36 @@ + + +{#snippet Fallback()} + +{/snippet} + + + {#if children} + {@render children?.()} + {:else} + {@render Fallback()} + {/if} + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar-year-select.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar-year-select.svelte new file mode 100644 index 0000000..898c2a4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar-year-select.svelte @@ -0,0 +1,47 @@ + + + + + {#snippet child({ props, yearItems, selectedYearItem })} + + + {/snippet} + + diff --git a/template/apps/web/src/lib/components/ui/calendar/calendar.svelte b/template/apps/web/src/lib/components/ui/calendar/calendar.svelte new file mode 100644 index 0000000..74abd6a --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/calendar.svelte @@ -0,0 +1,115 @@ + + + + + {#snippet children({ months, weekdays })} + + + + + + {#each months as month, monthIndex (month)} + + + + + + + + {#each weekdays as weekday (weekday)} + + {weekday.slice(0, 2)} + + {/each} + + + + {#each month.weeks as weekDates (weekDates)} + + {#each weekDates as date (date)} + + {#if day} + {@render day({ + day: date, + outsideMonth: !isEqualMonth(date, month.value), + })} + {:else} + + {/if} + + {/each} + + {/each} + + + + {/each} + + {/snippet} + diff --git a/template/apps/web/src/lib/components/ui/calendar/index.ts b/template/apps/web/src/lib/components/ui/calendar/index.ts new file mode 100644 index 0000000..f3a16d2 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/calendar/index.ts @@ -0,0 +1,40 @@ +import Root from "./calendar.svelte"; +import Cell from "./calendar-cell.svelte"; +import Day from "./calendar-day.svelte"; +import Grid from "./calendar-grid.svelte"; +import Header from "./calendar-header.svelte"; +import Months from "./calendar-months.svelte"; +import GridRow from "./calendar-grid-row.svelte"; +import Heading from "./calendar-heading.svelte"; +import GridBody from "./calendar-grid-body.svelte"; +import GridHead from "./calendar-grid-head.svelte"; +import HeadCell from "./calendar-head-cell.svelte"; +import NextButton from "./calendar-next-button.svelte"; +import PrevButton from "./calendar-prev-button.svelte"; +import MonthSelect from "./calendar-month-select.svelte"; +import YearSelect from "./calendar-year-select.svelte"; +import Month from "./calendar-month.svelte"; +import Nav from "./calendar-nav.svelte"; +import Caption from "./calendar-caption.svelte"; + +export { + Day, + Cell, + Grid, + Header, + Months, + GridRow, + Heading, + GridBody, + GridHead, + HeadCell, + NextButton, + PrevButton, + Nav, + Month, + YearSelect, + MonthSelect, + Caption, + // + Root as Calendar, +}; diff --git a/template/apps/web/src/lib/components/ui/card/card-action.svelte b/template/apps/web/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..7c48844 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/card/card-content.svelte b/template/apps/web/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..4f60ee3 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/card/card-description.svelte b/template/apps/web/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..8366534 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

    + {@render children?.()} +

    diff --git a/template/apps/web/src/lib/components/ui/card/card-footer.svelte b/template/apps/web/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..17aa09a --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/card/card-header.svelte b/template/apps/web/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..4ec42c5 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/card/card-title.svelte b/template/apps/web/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..b2a4368 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/card/card.svelte b/template/apps/web/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..86226bb --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/card.svelte @@ -0,0 +1,22 @@ + + +
    img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col", className)} + {...restProps} +> + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/card/index.ts b/template/apps/web/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/template/apps/web/src/lib/components/ui/carousel/carousel-content.svelte b/template/apps/web/src/lib/components/ui/carousel/carousel-content.svelte new file mode 100644 index 0000000..84c71f8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/carousel-content.svelte @@ -0,0 +1,43 @@ + + +
    +
    + {@render children?.()} +
    +
    diff --git a/template/apps/web/src/lib/components/ui/carousel/carousel-item.svelte b/template/apps/web/src/lib/components/ui/carousel/carousel-item.svelte new file mode 100644 index 0000000..ebf1649 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/carousel-item.svelte @@ -0,0 +1,30 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/carousel/carousel-next.svelte b/template/apps/web/src/lib/components/ui/carousel/carousel-next.svelte new file mode 100644 index 0000000..7a3fdb3 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/carousel-next.svelte @@ -0,0 +1,39 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/carousel/carousel-previous.svelte b/template/apps/web/src/lib/components/ui/carousel/carousel-previous.svelte new file mode 100644 index 0000000..29cd858 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/carousel-previous.svelte @@ -0,0 +1,39 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/carousel/carousel.svelte b/template/apps/web/src/lib/components/ui/carousel/carousel.svelte new file mode 100644 index 0000000..b3933b8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/carousel.svelte @@ -0,0 +1,94 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/carousel/context.ts b/template/apps/web/src/lib/components/ui/carousel/context.ts new file mode 100644 index 0000000..a5fd74f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/context.ts @@ -0,0 +1,58 @@ +import type { WithElementRef } from "$lib/utils.js"; +import type { + EmblaCarouselSvelteType, + default as emblaCarouselSvelte, +} from "embla-carousel-svelte"; +import { getContext, hasContext, setContext } from "svelte"; +import type { HTMLAttributes } from "svelte/elements"; + +export type CarouselAPI = + NonNullable["on:emblaInit"]> extends ( + evt: CustomEvent + ) => void + ? CarouselAPI + : never; + +type EmblaCarouselConfig = NonNullable[1]>; + +export type CarouselOptions = EmblaCarouselConfig["options"]; +export type CarouselPlugins = EmblaCarouselConfig["plugins"]; + +//// + +export type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugins; + setApi?: (api: CarouselAPI | undefined) => void; + orientation?: "horizontal" | "vertical"; +} & WithElementRef>; + +const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT"); + +export type EmblaContext = { + api: CarouselAPI | undefined; + orientation: "horizontal" | "vertical"; + scrollNext: () => void; + scrollPrev: () => void; + canScrollNext: boolean; + canScrollPrev: boolean; + handleKeyDown: (e: KeyboardEvent) => void; + options: CarouselOptions; + plugins: CarouselPlugins; + onInit: (e: CustomEvent) => void; + scrollTo: (index: number, jump?: boolean) => void; + scrollSnaps: number[]; + selectedIndex: number; +}; + +export function setEmblaContext(config: EmblaContext): EmblaContext { + setContext(EMBLA_CAROUSEL_CONTEXT, config); + return config; +} + +export function getEmblaContext(name = "This component") { + if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) { + throw new Error(`${name} must be used within a component`); + } + return getContext>(EMBLA_CAROUSEL_CONTEXT); +} diff --git a/template/apps/web/src/lib/components/ui/carousel/index.ts b/template/apps/web/src/lib/components/ui/carousel/index.ts new file mode 100644 index 0000000..957fc74 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/carousel/index.ts @@ -0,0 +1,19 @@ +import Root from "./carousel.svelte"; +import Content from "./carousel-content.svelte"; +import Item from "./carousel-item.svelte"; +import Previous from "./carousel-previous.svelte"; +import Next from "./carousel-next.svelte"; + +export { + Root, + Content, + Item, + Previous, + Next, + // + Root as Carousel, + Content as CarouselContent, + Item as CarouselItem, + Previous as CarouselPrevious, + Next as CarouselNext, +}; diff --git a/template/apps/web/src/lib/components/ui/chart/chart-container.svelte b/template/apps/web/src/lib/components/ui/chart/chart-container.svelte new file mode 100644 index 0000000..1eb8e39 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/chart/chart-container.svelte @@ -0,0 +1,80 @@ + + +
    + + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/chart/chart-style.svelte b/template/apps/web/src/lib/components/ui/chart/chart-style.svelte new file mode 100644 index 0000000..ea1b3b2 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/chart/chart-style.svelte @@ -0,0 +1,37 @@ + + +{#if themeContents} + {#key id} + + {themeContents} + + {/key} +{/if} diff --git a/template/apps/web/src/lib/components/ui/chart/chart-tooltip.svelte b/template/apps/web/src/lib/components/ui/chart/chart-tooltip.svelte new file mode 100644 index 0000000..e0da9ca --- /dev/null +++ b/template/apps/web/src/lib/components/ui/chart/chart-tooltip.svelte @@ -0,0 +1,184 @@ + + +{#snippet TooltipLabel()} + {#if formattedLabel} +
    + {#if typeof formattedLabel === "function"} + {@render formattedLabel()} + {:else} + {formattedLabel} + {/if} +
    + {/if} +{/snippet} + + +
    + {#if !nestLabel} + {@render TooltipLabel()} + {/if} +
    + {#each visibleSeries as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.label || "value"}`} + {@const itemConfig = getPayloadConfigFromPayload( + chart.config, + item, + key, + chartCtx.tooltip.data + )} + {@const indicatorColor = color || item.config?.color || item.color} +
    svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5", + indicator === "dot" && "items-center" + )} + > + {#if formatter && item.value !== undefined && item.label} + {@render formatter({ + value: item.value, + name: item.label, + item, + index: i, + payload: visibleSeries, + })} + {:else} + {#if itemConfig?.icon} + + {:else if !hideIndicator} +
    + {/if} +
    +
    + {#if nestLabel} + {@render TooltipLabel()} + {/if} + + {itemConfig?.label || item.label} + +
    + {#if item.value !== undefined} + + {item.value.toLocaleString()} + + {/if} +
    + {/if} +
    + {/each} +
    +
    +
    diff --git a/template/apps/web/src/lib/components/ui/chart/chart-utils.ts b/template/apps/web/src/lib/components/ui/chart/chart-utils.ts new file mode 100644 index 0000000..a289e35 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/chart/chart-utils.ts @@ -0,0 +1,68 @@ +import type { Tooltip } from "layerchart"; +import { getContext, setContext, type Component, type Snippet } from "svelte"; + +export const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: string; + icon?: Component; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +export type ExtractSnippetParams = T extends Snippet<[infer P]> ? P : never; + +export type TooltipPayload = Tooltip.TooltipSeries; + +// Helper to extract item config from a payload. +export function getPayloadConfigFromPayload( + config: ChartConfig, + payload: TooltipPayload, + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record | null +) { + if (typeof payload !== "object" || payload === null) return undefined; + + const payloadConfig = + "config" in payload && typeof payload.config === "object" && payload.config !== null + ? payload.config + : undefined; + + let configLabelKey: string = key; + + if (payload.key === key) { + configLabelKey = payload.key; + } else if (payload.label === key) { + configLabelKey = payload.label; + } else if (key in payload && typeof payload[key as keyof typeof payload] === "string") { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadConfig !== undefined && + key in payloadConfig && + typeof payloadConfig[key as keyof typeof payloadConfig] === "string" + ) { + configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string; + } else if (data != null && key in data && typeof data[key] === "string") { + configLabelKey = data[key] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; +} + +type ChartContextValue = { + config: ChartConfig; +}; + +const chartContextKey = Symbol("chart-context"); + +export function setChartContext(value: ChartContextValue) { + return setContext(chartContextKey, value); +} + +export function useChart() { + return getContext(chartContextKey); +} diff --git a/template/apps/web/src/lib/components/ui/chart/index.ts b/template/apps/web/src/lib/components/ui/chart/index.ts new file mode 100644 index 0000000..f22375e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/chart/index.ts @@ -0,0 +1,6 @@ +import ChartContainer from "./chart-container.svelte"; +import ChartTooltip from "./chart-tooltip.svelte"; + +export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js"; + +export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip }; diff --git a/template/apps/web/src/lib/components/ui/checkbox/checkbox.svelte b/template/apps/web/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..39af8e1 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,39 @@ + + + + {#snippet children({ checked, indeterminate })} +
    + {#if checked} + + {:else if indeterminate} + + {/if} +
    + {/snippet} +
    diff --git a/template/apps/web/src/lib/components/ui/checkbox/index.ts b/template/apps/web/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/template/apps/web/src/lib/components/ui/collapsible/collapsible-content.svelte b/template/apps/web/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..bdabb55 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/template/apps/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..ece7ad6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/collapsible/collapsible.svelte b/template/apps/web/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..39cdd4e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/collapsible/index.ts b/template/apps/web/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..169b479 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Root from "./collapsible.svelte"; +import Trigger from "./collapsible-trigger.svelte"; +import Content from "./collapsible-content.svelte"; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/template/apps/web/src/lib/components/ui/command/command-dialog.svelte b/template/apps/web/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..024c76c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,42 @@ + + + + + {title} + {description} + + + + + diff --git a/template/apps/web/src/lib/components/ui/command/command-empty.svelte b/template/apps/web/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..4cfd990 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/command/command-group.svelte b/template/apps/web/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..0712eee --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,32 @@ + + + + {#if heading} + + {heading} + + {/if} + + diff --git a/template/apps/web/src/lib/components/ui/command/command-input.svelte b/template/apps/web/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..2881f6f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,31 @@ + + +
    + + + + + + +
    diff --git a/template/apps/web/src/lib/components/ui/command/command-item.svelte b/template/apps/web/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..4246840 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,25 @@ + + + + {@render children?.()} + + diff --git a/template/apps/web/src/lib/components/ui/command/command-link-item.svelte b/template/apps/web/src/lib/components/ui/command/command-link-item.svelte new file mode 100644 index 0000000..ada6d2c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-link-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/command/command-list.svelte b/template/apps/web/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..e97747f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/command/command-loading.svelte b/template/apps/web/src/lib/components/ui/command/command-loading.svelte new file mode 100644 index 0000000..19dd298 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-loading.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/command/command-separator.svelte b/template/apps/web/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..35c4c95 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/command/command-shortcut.svelte b/template/apps/web/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..5322e6a --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/command/command.svelte b/template/apps/web/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..74a415b --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/command.svelte @@ -0,0 +1,25 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/command/index.ts b/template/apps/web/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..5435fbe --- /dev/null +++ b/template/apps/web/src/lib/components/ui/command/index.ts @@ -0,0 +1,37 @@ +import Root from "./command.svelte"; +import Loading from "./command-loading.svelte"; +import Dialog from "./command-dialog.svelte"; +import Empty from "./command-empty.svelte"; +import Group from "./command-group.svelte"; +import Item from "./command-item.svelte"; +import Input from "./command-input.svelte"; +import List from "./command-list.svelte"; +import Separator from "./command-separator.svelte"; +import Shortcut from "./command-shortcut.svelte"; +import LinkItem from "./command-link-item.svelte"; + +export { + Root, + Dialog, + Empty, + Group, + Item, + LinkItem, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + LinkItem as CommandLinkItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading, +}; diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte new file mode 100644 index 0000000..da5bd30 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte @@ -0,0 +1,41 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-content.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-content.svelte new file mode 100644 index 0000000..fb34a60 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-content.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-group-heading.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-group-heading.svelte new file mode 100644 index 0000000..0c0f459 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-group-heading.svelte @@ -0,0 +1,21 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-group.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-group.svelte new file mode 100644 index 0000000..c7c1e06 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-item.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-item.svelte new file mode 100644 index 0000000..d1601b6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-label.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-label.svelte new file mode 100644 index 0000000..4b3b1fe --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-portal.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-portal.svelte new file mode 100644 index 0000000..96b1e3e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-radio-group.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-radio-group.svelte new file mode 100644 index 0000000..964cb55 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-radio-item.svelte new file mode 100644 index 0000000..24b7380 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-separator.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-separator.svelte new file mode 100644 index 0000000..e675367 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-shortcut.svelte new file mode 100644 index 0000000..3305abc --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub-content.svelte new file mode 100644 index 0000000..fd85c70 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte new file mode 100644 index 0000000..b1e9370 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub.svelte new file mode 100644 index 0000000..a03827b --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu-trigger.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu-trigger.svelte new file mode 100644 index 0000000..f6a1542 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/context-menu.svelte b/template/apps/web/src/lib/components/ui/context-menu/context-menu.svelte new file mode 100644 index 0000000..cfaefb3 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/context-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/context-menu/index.ts b/template/apps/web/src/lib/components/ui/context-menu/index.ts new file mode 100644 index 0000000..cbeaee1 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/context-menu/index.ts @@ -0,0 +1,52 @@ +import Root from "./context-menu.svelte"; +import Sub from "./context-menu-sub.svelte"; +import Portal from "./context-menu-portal.svelte"; +import Trigger from "./context-menu-trigger.svelte"; +import Group from "./context-menu-group.svelte"; +import RadioGroup from "./context-menu-radio-group.svelte"; +import Item from "./context-menu-item.svelte"; +import GroupHeading from "./context-menu-group-heading.svelte"; +import Content from "./context-menu-content.svelte"; +import Shortcut from "./context-menu-shortcut.svelte"; +import RadioItem from "./context-menu-radio-item.svelte"; +import Separator from "./context-menu-separator.svelte"; +import SubContent from "./context-menu-sub-content.svelte"; +import SubTrigger from "./context-menu-sub-trigger.svelte"; +import CheckboxItem from "./context-menu-checkbox-item.svelte"; +import Label from "./context-menu-label.svelte"; + +export { + Root, + Sub, + Portal, + Item, + GroupHeading, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as ContextMenu, + Sub as ContextMenuSub, + Portal as ContextMenuPortal, + Item as ContextMenuItem, + GroupHeading as ContextMenuGroupHeading, + Group as ContextMenuGroup, + Content as ContextMenuContent, + Trigger as ContextMenuTrigger, + Shortcut as ContextMenuShortcut, + RadioItem as ContextMenuRadioItem, + Separator as ContextMenuSeparator, + RadioGroup as ContextMenuRadioGroup, + SubContent as ContextMenuSubContent, + SubTrigger as ContextMenuSubTrigger, + CheckboxItem as ContextMenuCheckboxItem, + Label as ContextMenuLabel, +}; diff --git a/template/apps/web/src/lib/components/ui/data-table/data-table.svelte.ts b/template/apps/web/src/lib/components/ui/data-table/data-table.svelte.ts new file mode 100644 index 0000000..f3c30a7 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/data-table/data-table.svelte.ts @@ -0,0 +1,142 @@ +import { + type RowData, + type TableOptions, + type TableOptionsResolved, + type TableState, + type Updater, + createTable, +} from "@tanstack/table-core"; + +/** + * Creates a reactive TanStack table object for Svelte. + * @param options Table options to create the table with. + * @returns A reactive table object. + * @example + * ```svelte + * + * + * + * + * {#each table.getHeaderGroups() as headerGroup} + * + * {#each headerGroup.headers as header} + * + * {/each} + * + * {/each} + * + * + *
    + * + *
    + * ``` + */ +export function createSvelteTable(options: TableOptions) { + const resolvedOptions: TableOptionsResolved = mergeObjects( + { + state: {}, + onStateChange() {}, + renderFallbackValue: null, + mergeOptions: ( + defaultOptions: TableOptions, + options: Partial> + ) => { + return mergeObjects(defaultOptions, options); + }, + }, + options + ); + + const table = createTable(resolvedOptions); + let state = $state(table.initialState); + + function updateOptions() { + table.setOptions(() => { + return mergeObjects(resolvedOptions, options, { + state: mergeObjects(state, options.state || {}), + + onStateChange: (updater: Updater) => { + if (updater instanceof Function) state = updater(state); + else state = mergeObjects(state, updater); + + options.onStateChange?.(updater); + }, + }); + }); + } + + updateOptions(); + + $effect.pre(() => { + updateOptions(); + }); + + return table; +} + +type MaybeThunk = T | (() => T | null | undefined); +type Intersection = (T extends [infer H, ...infer R] + ? H & Intersection + : unknown) & {}; + +/** + * Lazily merges several objects (or thunks) while preserving + * getter semantics from every source. + * + * Proxy-based to avoid known WebKit recursion issue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mergeObjects[]>( + ...sources: Sources +): Intersection<{ [K in keyof Sources]: Sources[K] }> { + const resolve = (src: MaybeThunk): T | undefined => + typeof src === "function" ? (src() ?? undefined) : src; + + const findSourceWithKey = (key: PropertyKey) => { + for (let i = sources.length - 1; i >= 0; i--) { + const obj = resolve(sources[i]); + if (obj && key in obj) return obj; + } + return undefined; + }; + + return new Proxy(Object.create(null), { + get(_, key) { + const src = findSourceWithKey(key); + + return src?.[key as never]; + }, + + has(_, key) { + return !!findSourceWithKey(key); + }, + + ownKeys(): (string | symbol)[] { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const all = new Set(); + for (const s of sources) { + const obj = resolve(s); + if (obj) { + for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) { + all.add(k); + } + } + } + return [...all]; + }, + + getOwnPropertyDescriptor(_, key) { + const src = findSourceWithKey(key); + if (!src) return undefined; + return { + configurable: true, + enumerable: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: (src as any)[key], + writable: true, + }; + }, + }) as Intersection<{ [K in keyof Sources]: Sources[K] }>; +} diff --git a/template/apps/web/src/lib/components/ui/data-table/flex-render.svelte b/template/apps/web/src/lib/components/ui/data-table/flex-render.svelte new file mode 100644 index 0000000..ac82a58 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/data-table/flex-render.svelte @@ -0,0 +1,40 @@ + + +{#if typeof content === "string"} + {content} +{:else if content instanceof Function} + + + {@const result = content(context as any)} + {#if result instanceof RenderComponentConfig} + {@const { component: Component, props } = result} + + {:else if result instanceof RenderSnippetConfig} + {@const { snippet, params } = result} + {@render snippet({ ...params, attach })} + {:else} + {result} + {/if} +{/if} diff --git a/template/apps/web/src/lib/components/ui/data-table/index.ts b/template/apps/web/src/lib/components/ui/data-table/index.ts new file mode 100644 index 0000000..5f4e77e --- /dev/null +++ b/template/apps/web/src/lib/components/ui/data-table/index.ts @@ -0,0 +1,3 @@ +export { default as FlexRender } from "./flex-render.svelte"; +export { renderComponent, renderSnippet } from "./render-helpers.js"; +export { createSvelteTable } from "./data-table.svelte.js"; diff --git a/template/apps/web/src/lib/components/ui/data-table/render-helpers.ts b/template/apps/web/src/lib/components/ui/data-table/render-helpers.ts new file mode 100644 index 0000000..fa036d6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/data-table/render-helpers.ts @@ -0,0 +1,111 @@ +import type { Component, ComponentProps, Snippet } from "svelte"; + +/** + * A helper class to make it easy to identify Svelte components in + * `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderComponentConfig} + * {@const { component: Component, props } = result} + * + * {/if} + * ``` + */ +export class RenderComponentConfig { + component: TComponent; + props: ComponentProps | Record; + constructor( + component: TComponent, + props: ComponentProps | Record = {} + ) { + this.component = component; + this.props = props; + } +} + +/** + * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderSnippetConfig} + * {@const { snippet, params } = result} + * {@render snippet(params)} + * {/if} + * ``` + */ +export class RenderSnippetConfig { + snippet: Snippet<[TProps]>; + params: TProps; + constructor(snippet: Snippet<[TProps]>, params: TProps) { + this.snippet = snippet; + this.params = params; + } +} + +/** + * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. + * + * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. + * + * @param component A Svelte component + * @param props The props to pass to `component` + * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * header: header => renderComponent(SortHeader, { label: 'Name', header }), + * }), + * columnHelper.accessor('state', { + * header: header => renderComponent(SortHeader, { label: 'State', header }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderComponent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Component, + Props extends ComponentProps, +>(component: T, props: Props = {} as Props) { + return new RenderComponentConfig(component, props); +} + +/** + * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. + * + * The snippet must only take one parameter. + * + * This is only to be used with Snippets - use `renderComponent` for Svelte Components. + * + * @param snippet + * @param params + * @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), + * }), + * columnHelper.accessor('state', { + * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderSnippet(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) { + return new RenderSnippetConfig(snippet, params); +} diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-close.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..de68f2f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,11 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-content.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..6c963e4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,48 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-description.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..bd220ac --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..2f54b2c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,32 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-header.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..8164f0f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..19f69f0 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-title.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..493f497 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..589ee0c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,11 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/dialog.svelte b/template/apps/web/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dialog/index.ts b/template/apps/web/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-close.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-close.svelte new file mode 100644 index 0000000..95c2479 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-content.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-content.svelte new file mode 100644 index 0000000..c389dd6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-content.svelte @@ -0,0 +1,33 @@ + + + + + + + {@render children?.()} + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-description.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-description.svelte new file mode 100644 index 0000000..ded26d2 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-footer.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-footer.svelte new file mode 100644 index 0000000..56a1fc6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-header.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-header.svelte new file mode 100644 index 0000000..861f956 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-nested.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-nested.svelte new file mode 100644 index 0000000..834af94 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-nested.svelte @@ -0,0 +1,12 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-overlay.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-overlay.svelte new file mode 100644 index 0000000..84df1e8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-overlay.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-portal.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-portal.svelte new file mode 100644 index 0000000..5a0dd74 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-title.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-title.svelte new file mode 100644 index 0000000..ba99287 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer-trigger.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer-trigger.svelte new file mode 100644 index 0000000..f1877d8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/drawer.svelte b/template/apps/web/src/lib/components/ui/drawer/drawer.svelte new file mode 100644 index 0000000..0cb57ff --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/drawer.svelte @@ -0,0 +1,12 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/drawer/index.ts b/template/apps/web/src/lib/components/ui/drawer/index.ts new file mode 100644 index 0000000..1656cac --- /dev/null +++ b/template/apps/web/src/lib/components/ui/drawer/index.ts @@ -0,0 +1,38 @@ +import Root from "./drawer.svelte"; +import Content from "./drawer-content.svelte"; +import Description from "./drawer-description.svelte"; +import Overlay from "./drawer-overlay.svelte"; +import Footer from "./drawer-footer.svelte"; +import Header from "./drawer-header.svelte"; +import Title from "./drawer-title.svelte"; +import NestedRoot from "./drawer-nested.svelte"; +import Close from "./drawer-close.svelte"; +import Trigger from "./drawer-trigger.svelte"; +import Portal from "./drawer-portal.svelte"; + +export { + Root, + NestedRoot, + Content, + Description, + Overlay, + Footer, + Header, + Title, + Trigger, + Portal, + Close, + + // + Root as Drawer, + NestedRoot as DrawerNestedRoot, + Content as DrawerContent, + Description as DrawerDescription, + Overlay as DrawerOverlay, + Footer as DrawerFooter, + Header as DrawerHeader, + Title as DrawerTitle, + Trigger as DrawerTrigger, + Portal as DrawerPortal, + Close as DrawerClose, +}; diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..e0e1971 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..e12c9a4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,44 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..1442e33 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..433540f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..20713df --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..05a9e64 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..274cfef --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..f7e4dbf --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,34 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..45f82af --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..ed7cc85 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..8de5cf1 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..5e16f67 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..f044581 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..cb4bc62 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/dropdown-menu/index.ts b/template/apps/web/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..7850c6a --- /dev/null +++ b/template/apps/web/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/template/apps/web/src/lib/components/ui/empty/empty-content.svelte b/template/apps/web/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..cc17823 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/empty/empty-description.svelte b/template/apps/web/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..9e8cd78 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
    a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/empty/empty-header.svelte b/template/apps/web/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..7112f66 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/empty/empty-media.svelte b/template/apps/web/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..24fe55d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/empty/empty-title.svelte b/template/apps/web/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..346c2ec --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/empty/empty.svelte b/template/apps/web/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..e16c45c --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/empty/index.ts b/template/apps/web/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..ae4c106 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/template/apps/web/src/lib/components/ui/field/field-content.svelte b/template/apps/web/src/lib/components/ui/field/field-content.svelte new file mode 100644 index 0000000..1545c43 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/field/field-description.svelte b/template/apps/web/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 0000000..f033c10 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,25 @@ + + +

    a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +

    diff --git a/template/apps/web/src/lib/components/ui/field/field-error.svelte b/template/apps/web/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 0000000..326cf59 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,58 @@ + + +{#if hasContent} + +{/if} diff --git a/template/apps/web/src/lib/components/ui/field/field-group.svelte b/template/apps/web/src/lib/components/ui/field/field-group.svelte new file mode 100644 index 0000000..3795195 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-group.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/field/field-label.svelte b/template/apps/web/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 0000000..6d9ec73 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,25 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/field/field-legend.svelte b/template/apps/web/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 0000000..e3d8deb --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,24 @@ + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/field/field-separator.svelte b/template/apps/web/src/lib/components/ui/field/field-separator.svelte new file mode 100644 index 0000000..a1ab858 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-separator.svelte @@ -0,0 +1,35 @@ + + +
    + + {#if children} + + {@render children()} + + {/if} +
    diff --git a/template/apps/web/src/lib/components/ui/field/field-set.svelte b/template/apps/web/src/lib/components/ui/field/field-set.svelte new file mode 100644 index 0000000..27ee330 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-set.svelte @@ -0,0 +1,20 @@ + + +
    [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", className)} + {...restProps} +> + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/field/field-title.svelte b/template/apps/web/src/lib/components/ui/field/field-title.svelte new file mode 100644 index 0000000..f7e2f08 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/field/field.svelte b/template/apps/web/src/lib/components/ui/field/field.svelte new file mode 100644 index 0000000..03d23c0 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/field.svelte @@ -0,0 +1,47 @@ + + + + +
    + {@render children?.()} +
    diff --git a/template/apps/web/src/lib/components/ui/field/index.ts b/template/apps/web/src/lib/components/ui/field/index.ts new file mode 100644 index 0000000..a644a95 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/field/index.ts @@ -0,0 +1,33 @@ +import Field from "./field.svelte"; +import Set from "./field-set.svelte"; +import Legend from "./field-legend.svelte"; +import Group from "./field-group.svelte"; +import Content from "./field-content.svelte"; +import Label from "./field-label.svelte"; +import Title from "./field-title.svelte"; +import Description from "./field-description.svelte"; +import Separator from "./field-separator.svelte"; +import Error from "./field-error.svelte"; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError, +}; diff --git a/template/apps/web/src/lib/components/ui/form/form-button.svelte b/template/apps/web/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 0000000..48d3936 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/input-group/input-group-input.svelte b/template/apps/web/src/lib/components/ui/input-group/input-group-input.svelte new file mode 100644 index 0000000..316c0cb --- /dev/null +++ b/template/apps/web/src/lib/components/ui/input-group/input-group-input.svelte @@ -0,0 +1,20 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/input-group/input-group-text.svelte b/template/apps/web/src/lib/components/ui/input-group/input-group-text.svelte new file mode 100644 index 0000000..d6ab315 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/input-group/input-group-text.svelte @@ -0,0 +1,19 @@ + + + + {@render children?.()} + diff --git a/template/apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte b/template/apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte new file mode 100644 index 0000000..9fbabe6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/input-group/input-group-textarea.svelte @@ -0,0 +1,20 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/toggle-group/index.ts b/template/apps/web/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000..12b14b9 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Root from "./toggle-group.svelte"; +import Item from "./toggle-group-item.svelte"; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem, +}; diff --git a/template/apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/template/apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000..904bf70 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,35 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte b/template/apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..3d67cb8 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,75 @@ + + + + + + diff --git a/template/apps/web/src/lib/components/ui/toggle/index.ts b/template/apps/web/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000..8cb2936 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from "./toggle.svelte"; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants, +} from "./toggle.svelte"; + +export { + Root, + // + Root as Toggle, +}; diff --git a/template/apps/web/src/lib/components/ui/toggle/toggle.svelte b/template/apps/web/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..eca68b1 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,51 @@ + + + + + diff --git a/template/apps/web/src/lib/components/ui/tooltip/index.ts b/template/apps/web/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..1718604 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/template/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte b/template/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..4f27fa3 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/template/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte b/template/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..d234f7d --- /dev/null +++ b/template/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte b/template/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..6dba9a6 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/template/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/template/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/template/apps/web/src/lib/components/ui/tooltip/tooltip.svelte b/template/apps/web/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..44bee9f --- /dev/null +++ b/template/apps/web/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/template/apps/web/src/lib/getconnectrouter.ts b/template/apps/web/src/lib/getconnectrouter.ts new file mode 100644 index 0000000..8ee292c --- /dev/null +++ b/template/apps/web/src/lib/getconnectrouter.ts @@ -0,0 +1,8 @@ +import {createRouter} from '@<@var(context.project.name)>/rpc' + +const router = createRouter("http://127.0.0.1:8080") + +export const getRouter = () => { + return router; +} + diff --git a/template/apps/web/src/lib/hooks/is-mobile.svelte.ts b/template/apps/web/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/template/apps/web/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/template/apps/web/src/lib/index.ts b/template/apps/web/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/template/apps/web/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/template/apps/web/src/lib/todocollectionscontext.ts b/template/apps/web/src/lib/todocollectionscontext.ts new file mode 100644 index 0000000..479b6b3 --- /dev/null +++ b/template/apps/web/src/lib/todocollectionscontext.ts @@ -0,0 +1,5 @@ +import { createContext } from "svelte"; +import { type CollectionImpl } from '@tanstack/svelte-db' +import type { Todo } from "@<@var(context.project.name)>/rpc"; +import { type ExtractPayload } from "./utils"; +export const [getTodoCollection,setTodoCollection] = createContext,string>>() diff --git a/template/apps/web/src/lib/utils.ts b/template/apps/web/src/lib/utils.ts new file mode 100644 index 0000000..93ccb61 --- /dev/null +++ b/template/apps/web/src/lib/utils.ts @@ -0,0 +1,14 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; +import {type Message} from "@bufbuild/protobuf" +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; +export type ExtractPayload = Omit>; diff --git a/template/apps/web/src/routes/+layout.svelte b/template/apps/web/src/routes/+layout.svelte new file mode 100644 index 0000000..e3dbf37 --- /dev/null +++ b/template/apps/web/src/routes/+layout.svelte @@ -0,0 +1,63 @@ + + + + + + {@render children()} + + +
    + {#each locales as locale} + {locale} + {/each} +
    diff --git a/template/apps/web/src/routes/+page.svelte b/template/apps/web/src/routes/+page.svelte new file mode 100644 index 0000000..ac76563 --- /dev/null +++ b/template/apps/web/src/routes/+page.svelte @@ -0,0 +1,11 @@ + + +
    + + +
    diff --git a/template/apps/web/src/routes/+page.ts b/template/apps/web/src/routes/+page.ts new file mode 100644 index 0000000..0b79dc8 --- /dev/null +++ b/template/apps/web/src/routes/+page.ts @@ -0,0 +1,7 @@ +import type { PageLoad } from './$types' +import { getRouter } from "$lib/getconnectrouter" +export const load: PageLoad = async () => { + const router = getRouter(); + const todos = await router.todos.listTodos({}) + return {todos: todos.todos}; +} diff --git a/template/apps/web/src/routes/layout.css b/template/apps/web/src/routes/layout.css new file mode 100644 index 0000000..00bce42 --- /dev/null +++ b/template/apps/web/src/routes/layout.css @@ -0,0 +1,130 @@ +@import 'tailwindcss'; +@import "tw-animate-css"; +@import "shadcn-svelte/tailwind.css"; +@import "@fontsource-variable/roboto"; + +@custom-variant dark (&:is(.dark *)); +@plugin '@tailwindcss/typography'; + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.148 0.004 228.8); + --card: oklch(1 0 0); + --card-foreground: oklch(0.148 0.004 228.8); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.148 0.004 228.8); + --primary: oklch(0.511 0.096 186.391); + --primary-foreground: oklch(0.984 0.014 180.72); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.963 0.002 197.1); + --muted-foreground: oklch(0.56 0.021 213.5); + --accent: oklch(0.963 0.002 197.1); + --accent-foreground: oklch(0.218 0.008 223.9); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.925 0.005 214.3); + --input: oklch(0.925 0.005 214.3); + --ring: oklch(0.723 0.014 214.4); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.987 0.002 197.1); + --sidebar-foreground: oklch(0.148 0.004 228.8); + --sidebar-primary: oklch(0.6 0.118 184.704); + --sidebar-primary-foreground: oklch(0.984 0.014 180.72); + --sidebar-accent: oklch(0.963 0.002 197.1); + --sidebar-accent-foreground: oklch(0.218 0.008 223.9); + --sidebar-border: oklch(0.925 0.005 214.3); + --sidebar-ring: oklch(0.723 0.014 214.4); +} + +.dark { + --background: oklch(0.148 0.004 228.8); + --foreground: oklch(0.987 0.002 197.1); + --card: oklch(0.218 0.008 223.9); + --card-foreground: oklch(0.987 0.002 197.1); + --popover: oklch(0.218 0.008 223.9); + --popover-foreground: oklch(0.987 0.002 197.1); + --primary: oklch(0.437 0.078 188.216); + --primary-foreground: oklch(0.984 0.014 180.72); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.275 0.011 216.9); + --muted-foreground: oklch(0.723 0.014 214.4); + --accent: oklch(0.275 0.011 216.9); + --accent-foreground: oklch(0.987 0.002 197.1); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.56 0.021 213.5); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.218 0.008 223.9); + --sidebar-foreground: oklch(0.987 0.002 197.1); + --sidebar-primary: oklch(0.704 0.14 182.503); + --sidebar-primary-foreground: oklch(0.277 0.046 192.524); + --sidebar-accent: oklch(0.275 0.011 216.9); + --sidebar-accent-foreground: oklch(0.987 0.002 197.1); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.56 0.021 213.5); +} + +@theme inline { + --font-sans: 'Roboto Variable', sans-serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/template/apps/web/static/robots.txt b/template/apps/web/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/template/apps/web/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/template/apps/web/svelte.config.js b/template/apps/web/svelte.config.js new file mode 100644 index 0000000..58d330b --- /dev/null +++ b/template/apps/web/svelte.config.js @@ -0,0 +1,24 @@ +import adapter from '@sveltejs/adapter-auto'; +import { relative, sep } from 'node:path'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + compilerOptions: { + // defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6. + runes: ({ filename }) => { + const relativePath = relative(import.meta.dirname, filename); + const pathSegments = relativePath.toLowerCase().split(sep); + const isExternalLibrary = pathSegments.includes('node_modules'); + + return isExternalLibrary ? undefined : true; + } + }, + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/template/apps/web/tsconfig.json b/template/apps/web/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/template/apps/web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/template/apps/web/vite.config.ts b/template/apps/web/vite.config.ts new file mode 100644 index 0000000..0fbc1a4 --- /dev/null +++ b/template/apps/web/vite.config.ts @@ -0,0 +1,12 @@ +import { paraglideVitePlugin } from '@inlang/paraglide-js'; +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + tailwindcss(), + sveltekit(), + paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' }) + ] +}); diff --git a/template/devenv.nix b/template/devenv.nix new file mode 100644 index 0000000..a33efe6 --- /dev/null +++ b/template/devenv.nix @@ -0,0 +1,52 @@ +{ pkgs, lib, config, inputs, ... }: + +{ + packages = [ + pkgs.bun + pkgs.watchexec + pkgs.sqlc + pkgs.dbmate + pkgs.air + pkgs.buf + pkgs.protoc-gen-go + pkgs.protoc-gen-connect-go + pkgs.protoc-gen-es + pkgs.cobra-cli + ]; + + languages.go.enable = true; + languages.typescript.enable = true; + services.postgres = { + enable = true; + listen_addresses = "127.0.0.1"; + initialDatabases = [ + { + name = "<@var(context.project.name)>"; + user = "pp"; + pass = "<@var(context.project.name)>"; + } + ]; + }; + processes = { + air = { + exec = "air"; + cwd = "./services/api"; + }; + protowatcher = { + exec = "watchexec -r -e proto buf generate"; + cwd = "./packages/proto"; + }; + protojswatcher = { + exec = "watchexec -e js,ts -w ./packages/rpc/src -r bun run ./scripts/gen-rpc-index.ts"; + cwd = "./"; + }; + sqlwatcher = { + exec = "watchexec -w -r db -e sql sqlc generate"; + cwd = "./services/api"; + }; + bundev = { + exec = "bun dev"; + cwd = "./apps/web"; + }; + }; +} diff --git a/template/devenv.yaml b/template/devenv.yaml new file mode 100644 index 0000000..116a2ad --- /dev/null +++ b/template/devenv.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend diff --git a/template/package.json b/template/package.json new file mode 100644 index 0000000..8c3b883 --- /dev/null +++ b/template/package.json @@ -0,0 +1,12 @@ +{ + "name": "<@var(context.project.name)>", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "bun run --filter '*' dev", + "build": "bun run --filter '*' build" + } +} diff --git a/template/packages/proto/buf.gen.yaml b/template/packages/proto/buf.gen.yaml new file mode 100644 index 0000000..21a4a30 --- /dev/null +++ b/template/packages/proto/buf.gen.yaml @@ -0,0 +1,15 @@ +version: v2 +plugins: + - local: protoc-gen-go + out: ../../services/api/gen + opt: paths=source_relative + - local: protoc-gen-connect-go + opt: + - paths=source_relative + out: ../../services/api/gen + + # TypeScript + - local: protoc-gen-es + out: ../rpc/src + include_imports: true + opt: target=ts diff --git a/template/packages/proto/buf.yaml b/template/packages/proto/buf.yaml new file mode 100644 index 0000000..00c96c2 --- /dev/null +++ b/template/packages/proto/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +deps: + - buf.build/bufbuild/protovalidate +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/template/packages/proto/todo/v1/todo.proto b/template/packages/proto/todo/v1/todo.proto new file mode 100644 index 0000000..af80c1b --- /dev/null +++ b/template/packages/proto/todo/v1/todo.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; +package todo.v1; + +option go_package = "<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1;todov1"; + +service TodoService { + rpc CreateTodo (CreateTodoRequest) returns (CreateTodoResponse); + rpc ListTodos (ListTodosRequest) returns (ListTodosResponse); + rpc UpdateTodo (UpdateTodoRequest) returns (UpdateTodoResponse); + rpc DeleteTodo (DeleteTodoRequest) returns (DeleteTodoResponse); +} + +message Todo { + optional string id = 1; + string task = 2; + optional string created_at = 3; + optional string updates_at = 4; + optional bool done = 5; + +} + +message DeleteTodoRequest { + Todo todo = 1; +} + +message DeleteTodoResponse {} + +message CreateTodoRequest { + Todo todo = 1; +} + +message CreateTodoResponse { + Todo todo = 1; +} +message ListTodosRequest { +} + +message ListTodosResponse { + repeated Todo todos = 1; +} + +message UpdateTodoRequest { + Todo todo = 1; +} + +message UpdateTodoResponse { + Todo todo = 1; +} + diff --git a/template/packages/rpc/.gitignore b/template/packages/rpc/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/template/packages/rpc/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/template/packages/rpc/CLAUDE.md b/template/packages/rpc/CLAUDE.md new file mode 100644 index 0000000..764c1dd --- /dev/null +++ b/template/packages/rpc/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

    Hello, world!

    ; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/template/packages/rpc/README.md b/template/packages/rpc/README.md new file mode 100644 index 0000000..12949a3 --- /dev/null +++ b/template/packages/rpc/README.md @@ -0,0 +1,15 @@ +# rpc + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/template/packages/rpc/package.json b/template/packages/rpc/package.json new file mode 100644 index 0000000..d2e1902 --- /dev/null +++ b/template/packages/rpc/package.json @@ -0,0 +1,22 @@ +{ + "name": "@<@var(context.project.name)>/rpc", + "type": "module", + "main": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "types": "./index.ts" + } + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.11.0", + "@connectrpc/connect": "^2.1.1", + "@connectrpc/connect-web": "^2.1.1" + } +} diff --git a/template/packages/rpc/tsconfig.json b/template/packages/rpc/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/template/packages/rpc/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/template/scripts/.helix/languages.toml b/template/scripts/.helix/languages.toml new file mode 100644 index 0000000..1813968 --- /dev/null +++ b/template/scripts/.helix/languages.toml @@ -0,0 +1,4 @@ +[[language]] +name="typescript" +roots = ["package.json"] +language-servers = ["typescript-language-server"] diff --git a/template/scripts/gen-rpc-index.ts b/template/scripts/gen-rpc-index.ts new file mode 100644 index 0000000..0bdb137 --- /dev/null +++ b/template/scripts/gen-rpc-index.ts @@ -0,0 +1,324 @@ +import ts, { StringLiteral } from "typescript"; +import { resolve, relative, dirname, join } from "path"; +import { Glob } from "bun"; + +const RPC_PKG = resolve(import.meta.dirname!, "../packages/rpc"); +const SRC_DIR = join(RPC_PKG, "src"); +const INDEX_FILE = join(RPC_PKG, "index.ts"); + +const glob = new Glob("**/*_pb.ts"); +const files = Array.from(glob.scanSync({ cwd: SRC_DIR, absolute: true })).sort(); + +if (files.length === 0) { + console.error("No _pb.ts files found in", SRC_DIR); + process.exit(1); +} + +// Read the rpc package's tsconfig so module resolution finds @bufbuild/protobuf +const tsconfigPath = join(RPC_PKG, "tsconfig.json"); +const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile); +const parsed = ts.parseJsonConfigFileContent(config, ts.sys, RPC_PKG); + +const program = ts.createProgram(files, { + ...parsed.options, + noEmit: true, +}); + +const checker = program.getTypeChecker(); +const factory = ts.factory; + +function hasTypeNamed(type: ts.Type, name: string): boolean { + if (type.aliasSymbol?.name === name) return true; + if (type.symbol?.name === name) return true; + const target = (type as any).target as ts.Type | undefined; + if (target?.symbol?.name === name) return true; + if (type.isIntersection()) { + return type.types.some((t) => hasTypeNamed(t, name)); + } + if (type.isUnion()) { + return type.types.some((t) => hasTypeNamed(t, name)); + } + return false; +} + +const isStringLiteral = (s: string | StringLiteral | undefined): s is StringLiteral => { + if (s === undefined) { + return false; + } + return (s as StringLiteral).forEachChild != undefined; +} + +const serviceNameToRouterPropertyName = (n:string) : string => { + return n.toLowerCase().replace('service','') + 's' +} + +const exportDecl = (exp: Map, typesOnly:boolean = false, from?: string | StringLiteral) => { + return factory.createExportDeclaration( + undefined, + typesOnly, + factory.createNamedExports( + Array.from(exp.entries()).map((exp) => { + return factory.createExportSpecifier( + exp[1], + undefined, + exp[0] + ) + }) + ), + ( + isStringLiteral(from) ? + from : + ( + from != undefined ? + factory.createStringLiteral(from) : + from + ) + ) + ) +} + +const importDecl = (imp: Map, from: string | StringLiteral, typesOnly: boolean = false) => { + return factory.createImportDeclaration( + undefined, + factory.createImportClause( + typesOnly, + undefined, + factory.createNamedImports( + Array.from(imp.entries()).map((k) => { + return factory.createImportSpecifier( + k[1], + undefined, + factory.createIdentifier(k[0]) + ) + }) + ) + ), + (isStringLiteral(from) ? from : factory.createStringLiteral(from)) + ) +} + +const statements: ts.Statement[] = []; +const serviceNames: string[] = []; +const typeNames: string[] = []; + +for (const filePath of files) { + const sourceFile = program.getSourceFile(filePath); + if (!sourceFile) continue; + + const symbol = checker.getSymbolAtLocation(sourceFile); + if (!symbol) continue; + + const exports = checker.getExportsOfModule(symbol); + + const fileServiceNames: string[] = []; + const fileTypeNames: string[] = []; + + for (const exp of exports) { + const declarations = exp.getDeclarations(); + if (!declarations || declarations.length === 0) continue; + + const decl = declarations[0]; + + if (ts.isTypeAliasDeclaration(decl) || ts.isInterfaceDeclaration(decl)) { + const type = checker.getDeclaredTypeOfSymbol(exp); + if (hasTypeNamed(type, "Message")) { + fileTypeNames.push(exp.getName()); + } + } else { + const type = checker.getTypeOfSymbolAtLocation(exp, decl); + if (hasTypeNamed(type, "GenService")) { + fileServiceNames.push(exp.getName()); + } + } + } + + let modulePath = "./" + relative(dirname(INDEX_FILE), filePath); + modulePath = modulePath.replace(/\.ts$/, ""); + + const moduleSpecifier = factory.createStringLiteral(modulePath); + statements.push( + importDecl( + new Map([ + ["Client", true], + ["createClient", false] + ]), + "@connectrpc/connect" + ) + ) + statements.push( + importDecl( + new Map([ + ["createConnectTransport", false] + ]), + "@connectrpc/connect-web" + ) + ) + if (fileServiceNames.length > 0) { + serviceNames.push(...fileServiceNames) + statements.push(importDecl( + new Map( + fileServiceNames.map((v) => { + return [v, false] as const; + }) + ), + moduleSpecifier + )) + } + + if (fileTypeNames.length > 0) { + typeNames.push(...fileTypeNames) + statements.push(importDecl( + new Map( + fileTypeNames.map((v) => { + return [v, false] as const; + }) + ), + moduleSpecifier, + true + )) + } +} + +statements.push( + exportDecl( + new Map(serviceNames.map((service) => { + return [service, false] as const; + })) + ) +) + +statements.push( + exportDecl( + new Map(typeNames.map((type) => { + return [type, false] as const; + })), + true + ) +) + +statements.push( + factory.createInterfaceDeclaration( + undefined, + factory.createIdentifier("Router"), + undefined, + undefined, + serviceNames.map((service) => { + return factory.createPropertySignature( + undefined, + serviceNameToRouterPropertyName(service), + undefined, + factory.createTypeReferenceNode( + factory.createIdentifier("Client"), + [ + factory.createTypeQueryNode( + factory.createIdentifier(service) + ) + ] + ) + ) + }) + ) +) + +statements.push(exportDecl( + new Map([ + ['Router', true] + ]) +)) + +statements.push(factory.createFunctionDeclaration( + undefined, + undefined, + 'createRouter', + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + 'baseUrl', + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined + ) + ], + factory.createTypeReferenceNode(factory.createIdentifier("Router"), undefined), + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + 'transport', + undefined, + undefined, + factory.createCallExpression( + factory.createIdentifier("createConnectTransport"), + undefined, + [ + factory.createObjectLiteralExpression([ + factory.createPropertyAssignment( + 'baseUrl', + factory.createIdentifier('baseUrl') + ) + ]) + ] + ) + ), + ], + ts.NodeFlags.Const + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + 'router', + undefined, + undefined, + factory.createObjectLiteralExpression( + [ + ...serviceNames.map((service) => { + return factory.createPropertyAssignment( + serviceNameToRouterPropertyName(service), + + factory.createCallExpression( + factory.createIdentifier('createClient'), + undefined, + [ + factory.createIdentifier(service), + factory.createIdentifier('transport') + ] + ) + ) + }) + ] + ) + ) + ], + ts.NodeFlags.Const + ) + ), + factory.createReturnStatement( + factory.createIdentifier('router') + ) + ], + true + ) +)) + +statements.push(exportDecl(new Map([ + ['createRouter', false] +]))) + +const outputFile = ts.createSourceFile(INDEX_FILE, "", ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); +const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + +const header = "// @generated by scripts/gen-rpc-index.ts — do not edit manually\n\n"; +const body = statements + .map((s) => printer.printNode(ts.EmitHint.Unspecified, s, outputFile)) + .join("\n"); + +await Bun.write(INDEX_FILE, header + body + "\n"); +console.log(`Wrote ${INDEX_FILE}`); diff --git a/template/scripts/package.json b/template/scripts/package.json new file mode 100644 index 0000000..6d062bb --- /dev/null +++ b/template/scripts/package.json @@ -0,0 +1,11 @@ +{ + "name": "@<@var(context.project.name)>/scripts", + "private": true, + "type": "module", + "dependencies": { + "typescript": "^5" + }, + "devDependencies": { + "@types/bun": "^1.3.11" + } +} diff --git a/template/services/api/.air.toml b/template/services/api/.air.toml new file mode 100644 index 0000000..9a2e4c2 --- /dev/null +++ b/template/services/api/.air.toml @@ -0,0 +1,58 @@ +#:schema https://json.schemastore.org/any.json + +env_files = [] +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + entrypoint = ["./tmp/main","serve"] + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + ignore_dangerous_root_dir = false + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + silent = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + app_start_timeout = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/template/services/api/.gitignore b/template/services/api/.gitignore new file mode 100644 index 0000000..a9a5aec --- /dev/null +++ b/template/services/api/.gitignore @@ -0,0 +1 @@ +tmp diff --git a/template/services/api/LICENSE b/template/services/api/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/template/services/api/cmd/configInit.go b/template/services/api/cmd/configInit.go new file mode 100644 index 0000000..540d2a1 --- /dev/null +++ b/template/services/api/cmd/configInit.go @@ -0,0 +1,45 @@ +/* +Copyright © 2026 NAME HERE +*/ +package cmd + +import ( + "fmt" + "os" + + "<@var(context.project.goprefix)>/<@var(context.project.name)>/config" + "github.com/spf13/cobra" +) + +// configInitCmd represents the configInit command +var configInitCmd = &cobra.Command{ + Use: "configInit", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + err := config.Write(*config.DefaultConf) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, +} + +func init() { + rootCmd.AddCommand(configInitCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // configInitCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // configInitCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/template/services/api/cmd/migrate.go b/template/services/api/cmd/migrate.go new file mode 100644 index 0000000..beee0d0 --- /dev/null +++ b/template/services/api/cmd/migrate.go @@ -0,0 +1,37 @@ +/* +Copyright © 2026 NAME HERE +*/ +package cmd + +import ( + "<@var(context.project.goprefix)>/<@var(context.project.name)>/db" + "github.com/spf13/cobra" +) + +// migrateCmd represents the migrate command +var migrateCmd = &cobra.Command{ + Use: "migrate", + Short: "run database migrations", + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + err := db.RunDbMigrations() + if err != nil { + return err + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(migrateCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // migrateCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // migrateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/template/services/api/cmd/root.go b/template/services/api/cmd/root.go new file mode 100644 index 0000000..6895c57 --- /dev/null +++ b/template/services/api/cmd/root.go @@ -0,0 +1,45 @@ +/* +Copyright © 2026 NAME HERE +*/ +package cmd + +import ( + "github.com/spf13/cobra" + "os" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "<@var(context.project.name)>", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.<@var(context.project.name)>.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/template/services/api/cmd/serve.go b/template/services/api/cmd/serve.go new file mode 100644 index 0000000..42e0d3e --- /dev/null +++ b/template/services/api/cmd/serve.go @@ -0,0 +1,81 @@ +/* +Copyright © 2026 NAME HERE +*/ +package cmd + +import ( + "<@var(context.project.goprefix)>/<@var(context.project.name)>/config" + "<@var(context.project.goprefix)>/<@var(context.project.name)>/db" + "<@var(context.project.goprefix)>/<@var(context.project.name)>/server/todo" + connectors "connectrpc.com/cors" + "context" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/rs/cors" + "github.com/spf13/cobra" + "net/http" + "strconv" + "strings" +) + +// serveCmd represents the serve command +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "start server", + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + err := config.Load() + if err != nil { + err := config.Write(*config.DefaultConf) + if err != nil { + return err + } + } + conf := *config.Conf + ctx := context.Background() + db_url := "postgres://" + conf.Database.User + ":" + conf.Database.Password + "@" + conf.Database.Host + ":" + strconv.Itoa(conf.Database.Port) + "/" + conf.Database.Name + db_url = db_url + "?sslmode=disable" + conn, err := pgxpool.New(ctx, db_url) + if err != nil { + panic(err.Error()) + } + err = db.RunDbMigrations() + if err != nil { + return err + } + db.Q = db.New(conn) + middleware := cors.New(cors.Options{ + AllowedOrigins: conf.Server.FrontendUrls, + AllowedMethods: connectors.AllowedMethods(), + AllowedHeaders: connectors.AllowedHeaders(), + ExposedHeaders: connectors.ExposedHeaders(), + Debug: true, + }) + mux := http.NewServeMux() + todoPath, todoHandler := todo.GetPathHandler() + mux.Handle(todoPath, todoHandler) + p := new(http.Protocols) + p.SetHTTP1(true) + p.SetUnencryptedHTTP2(true) + s := http.Server{ + Addr: strings.Join([]string{conf.Server.Host, strconv.Itoa(conf.Server.Port)}, ":"), + Handler: middleware.Handler(mux), + Protocols: p, + } + s.ListenAndServe() + return nil + }, +} + +func init() { + rootCmd.AddCommand(serveCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // serveCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/template/services/api/config/config.go b/template/services/api/config/config.go new file mode 100644 index 0000000..3bb7e9d --- /dev/null +++ b/template/services/api/config/config.go @@ -0,0 +1,81 @@ +package config + +import ( + "github.com/adrg/xdg" + "github.com/pelletier/go-toml" + "os" + "path/filepath" +) + +var confPath string +var Conf *Config +var DefaultConf *Config + +func init() { + confPath = filepath.Join(xdg.ConfigHome, "<@var(context.project.name)>", "config.toml") + Conf = &Config{} + DefaultConf = &Config{ + Server: Server{ + Host: "127.0.0.1", + Port: 8080, + FrontendUrls: []string{"http://localhost:5173"}, + }, + Database: Database{ + User: "pp", + Password: "<@var(context.project.name)>", + Host: "127.0.0.1", + Port: 5432, + Name: "<@var(context.project.name)>", + }, + } +} + +func Load() error { + confContent, err := os.ReadFile(confPath) + if err != nil { + return err + } + return toml.Unmarshal(confContent, Conf) + +} + +func Write(config Config) error { + confContent, err := toml.Marshal(config) + if err != nil { + return err + } + + dir := filepath.Dir(confPath) + + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + if err := os.WriteFile(confPath, confContent, 0644); err != nil { + return err + } + + if err := Load(); err != nil { + return err + } + return nil +} + +type Server struct { + Host string + Port int + FrontendUrls []string +} + +type Database struct { + User string + Password string + Host string + Port int + Name string +} + +type Config struct { + Server Server + Database Database +} diff --git a/template/services/api/db/db.go b/template/services/api/db/db.go new file mode 100644 index 0000000..9d485b5 --- /dev/null +++ b/template/services/api/db/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/template/services/api/db/dbfs.go b/template/services/api/db/dbfs.go new file mode 100644 index 0000000..6078ddf --- /dev/null +++ b/template/services/api/db/dbfs.go @@ -0,0 +1,6 @@ +package db + +import "embed" + +//go:embed migrations/*.sql +var MigrationsFs embed.FS diff --git a/template/services/api/db/lib.go b/template/services/api/db/lib.go new file mode 100644 index 0000000..c50948e --- /dev/null +++ b/template/services/api/db/lib.go @@ -0,0 +1,60 @@ +package db + +import ( + "<@var(context.project.goprefix)>/<@var(context.project.name)>/config" + "github.com/amacneil/dbmate/v2/pkg/dbmate" + _ "github.com/amacneil/dbmate/v2/pkg/driver/postgres" + "log" + "net/url" + "strconv" + "strings" +) + +var Q *Queries + +func RunDbMigrations() error { + + err := config.Load() + if err != nil { + return err + } + conf := *config.Conf + dbUrl, err := url.Parse(strings.Join([]string{ + "postgresql://", + conf.Database.User, + ":", + conf.Database.Password, + "@", + conf.Database.Host, + ":", + strconv.Itoa(conf.Database.Port), + "/", + conf.Database.Name, + }, "")) + if err != nil { + return err + } + db := dbmate.New(dbUrl) + db.FS = MigrationsFs + db.MigrationsDir = []string{"migrations"} + migrations, err := db.FindMigrations() + if err != nil { + return err + } + for _, m := range migrations { + log.Default().Println(m.Version, m.FilePath) + } + db.AutoDumpSchema = false + log.Default().Println("\nApplying...") + err = db.CreateAndMigrate() + if err != nil { + log.Default().Println(err.Error()) + } + err = db.DumpSchema() + if err != nil { + return err + } + + return nil + +} diff --git a/template/services/api/db/migrations/20260404121052_init.sql b/template/services/api/db/migrations/20260404121052_init.sql new file mode 100644 index 0000000..eaf1dc6 --- /dev/null +++ b/template/services/api/db/migrations/20260404121052_init.sql @@ -0,0 +1,10 @@ +-- migrate:up +CREATE TABLE todo ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + done bool DEFAULT false +); +-- migrate:down +DROP TABLE todo; diff --git a/template/services/api/db/models.go b/template/services/api/db/models.go new file mode 100644 index 0000000..f098474 --- /dev/null +++ b/template/services/api/db/models.go @@ -0,0 +1,17 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Todo struct { + ID pgtype.UUID + Task string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + Done pgtype.Bool +} diff --git a/template/services/api/db/query/todo.sql b/template/services/api/db/query/todo.sql new file mode 100644 index 0000000..027107d --- /dev/null +++ b/template/services/api/db/query/todo.sql @@ -0,0 +1,14 @@ +-- name: CreateTodo :one +insert into todo (id,task) values ($1,$2) returning *; + +-- name: ListTodos :many +select * from todo; + +-- name: GetTodo :one +select * from todo where id = $1 limit 1; + +-- name: UpdateTodo :one +update todo set task = $1, done = $2 where id = $3 returning *; + +-- name: DeleteTodo :exec +delete from todo where id = $1; diff --git a/template/services/api/db/schema.sql b/template/services/api/db/schema.sql new file mode 100644 index 0000000..9c3f874 --- /dev/null +++ b/template/services/api/db/schema.sql @@ -0,0 +1,86 @@ +\restrict dbmate + +-- Dumped from database version 17.9 +-- Dumped by pg_dump version 17.9 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; + + +-- +-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.schema_migrations ( + version character varying NOT NULL +); + + +-- +-- Name: todo; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.todo ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + task character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + done boolean DEFAULT false +); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + +-- +-- Name: todo todo_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.todo + ADD CONSTRAINT todo_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict dbmate + + +-- +-- Dbmate schema migrations +-- + +INSERT INTO public.schema_migrations (version) VALUES + ('20260404121052'); diff --git a/template/services/api/db/todo.sql.go b/template/services/api/db/todo.sql.go new file mode 100644 index 0000000..9f52618 --- /dev/null +++ b/template/services/api/db/todo.sql.go @@ -0,0 +1,113 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: todo.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createTodo = `-- name: CreateTodo :one +insert into todo (id,task) values ($1,$2) returning id, task, created_at, updated_at, done +` + +type CreateTodoParams struct { + ID pgtype.UUID + Task string +} + +func (q *Queries) CreateTodo(ctx context.Context, arg CreateTodoParams) (Todo, error) { + row := q.db.QueryRow(ctx, createTodo, arg.ID, arg.Task) + var i Todo + err := row.Scan( + &i.ID, + &i.Task, + &i.CreatedAt, + &i.UpdatedAt, + &i.Done, + ) + return i, err +} + +const deleteTodo = `-- name: DeleteTodo :exec +delete from todo where id = $1 +` + +func (q *Queries) DeleteTodo(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteTodo, id) + return err +} + +const getTodo = `-- name: GetTodo :one +select id, task, created_at, updated_at, done from todo where id = $1 limit 1 +` + +func (q *Queries) GetTodo(ctx context.Context, id pgtype.UUID) (Todo, error) { + row := q.db.QueryRow(ctx, getTodo, id) + var i Todo + err := row.Scan( + &i.ID, + &i.Task, + &i.CreatedAt, + &i.UpdatedAt, + &i.Done, + ) + return i, err +} + +const listTodos = `-- name: ListTodos :many +select id, task, created_at, updated_at, done from todo +` + +func (q *Queries) ListTodos(ctx context.Context) ([]Todo, error) { + rows, err := q.db.Query(ctx, listTodos) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Todo + for rows.Next() { + var i Todo + if err := rows.Scan( + &i.ID, + &i.Task, + &i.CreatedAt, + &i.UpdatedAt, + &i.Done, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateTodo = `-- name: UpdateTodo :one +update todo set task = $1, done = $2 where id = $3 returning id, task, created_at, updated_at, done +` + +type UpdateTodoParams struct { + Task string + Done pgtype.Bool + ID pgtype.UUID +} + +func (q *Queries) UpdateTodo(ctx context.Context, arg UpdateTodoParams) (Todo, error) { + row := q.db.QueryRow(ctx, updateTodo, arg.Task, arg.Done, arg.ID) + var i Todo + err := row.Scan( + &i.ID, + &i.Task, + &i.CreatedAt, + &i.UpdatedAt, + &i.Done, + ) + return i, err +} diff --git a/template/services/api/go.mod b/template/services/api/go.mod new file mode 100644 index 0000000..d8f71d5 --- /dev/null +++ b/template/services/api/go.mod @@ -0,0 +1,34 @@ +module <@var(context.project.goprefix)>/<@var(context.project.name)> + +go 1.25.7 + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 // indirect + buf.build/go/protovalidate v1.0.0 // indirect + cel.dev/expr v0.24.0 // indirect + connectrpc.com/connect v1.19.1 // indirect + connectrpc.com/cors v0.1.0 // indirect + connectrpc.com/validate v0.6.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/amacneil/dbmate/v2 v2.32.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lib/pq v1.12.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/template/services/api/go.sum b/template/services/api/go.sum new file mode 100644 index 0000000..331b399 --- /dev/null +++ b/template/services/api/go.sum @@ -0,0 +1,79 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 h1:DQLS/rRxLHuugVzjJU5AvOwD57pdFl9he/0O7e5P294= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1/go.mod h1:aY3zbkNan5F+cGm9lITDP6oxJIwu0dn9KjJuJjWaHkg= +buf.build/go/protovalidate v1.0.0 h1:IAG1etULddAy93fiBsFVhpj7es5zL53AfB/79CVGtyY= +buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc36vvCLW8= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ= +connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg= +connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4= +connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/amacneil/dbmate/v2 v2.32.0 h1:DJWt+NRWlphTv1HXWWcAU7gsJKm/Ov8/+8k6yqLz8vA= +github.com/amacneil/dbmate/v2 v2.32.0/go.mod h1:Kwax92bT+ZgqIXll9M+0QGuHVuu7O7AhsE+5p6PTqDc= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/template/services/api/main.go b/template/services/api/main.go new file mode 100644 index 0000000..3dbb805 --- /dev/null +++ b/template/services/api/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2026 NAME HERE + +*/ +package main + +import "<@var(context.project.goprefix)>/<@var(context.project.name)>/cmd" + +func main() { + cmd.Execute() +} diff --git a/template/services/api/server/todo/todo.go b/template/services/api/server/todo/todo.go new file mode 100644 index 0000000..d27d690 --- /dev/null +++ b/template/services/api/server/todo/todo.go @@ -0,0 +1,116 @@ +package todo + +import ( + "connectrpc.com/connect" + "connectrpc.com/validate" + "context" + "<@var(context.project.goprefix)>/<@var(context.project.name)>/db" + todov1 "<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1" + "<@var(context.project.goprefix)>/<@var(context.project.name)>/gen/todo/v1/todov1connect" + . "<@var(context.project.goprefix)>/<@var(context.project.name)>/utils" + "github.com/jackc/pgx/v5/pgtype" + "net/http" + "time" +) + +type TodoServer struct{} + +func (srv *TodoServer) CreateTodo(ctx context.Context, req *connect.Request[todov1.CreateTodoRequest]) (*connect.Response[todov1.CreateTodoResponse], error) { + var id pgtype.UUID + err := id.Scan(*req.Msg.Todo.Id) + if err != nil { + return nil, err + } + todo, err := db.Q.CreateTodo(ctx, db.CreateTodoParams{ + ID: id, + Task: req.Msg.Todo.Task, + }) + if err != nil { + return nil, err + } + return &connect.Response[todov1.CreateTodoResponse]{ + Msg: &todov1.CreateTodoResponse{ + Todo: &todov1.Todo{ + Id: StrPtr(todo.ID.String()), + Task: todo.Task, + CreatedAt: StrPtr(todo.CreatedAt.Time.Format(time.RFC3339)), + UpdatesAt: StrPtr(todo.UpdatedAt.Time.Format(time.RFC3339)), + Done: BoolPtr(todo.Done.Bool), + }, + }, + }, nil +} + +func (srv *TodoServer) ListTodos(ctx context.Context, req *connect.Request[todov1.ListTodosRequest]) (*connect.Response[todov1.ListTodosResponse], error) { + todos, err := db.Q.ListTodos(ctx) + if err != nil { + return nil, err + } + reponseTodos := []*todov1.Todo{} + for _, todo := range todos { + reponseTodos = append(reponseTodos, &todov1.Todo{ + Id: StrPtr(todo.ID.String()), + Task: todo.Task, + CreatedAt: StrPtr(todo.CreatedAt.Time.Format(time.RFC3339)), + UpdatesAt: StrPtr(todo.UpdatedAt.Time.Format(time.RFC3339)), + Done: BoolPtr(todo.Done.Bool), + }) + } + + return &connect.Response[todov1.ListTodosResponse]{ + Msg: &todov1.ListTodosResponse{ + Todos: reponseTodos, + }, + }, nil +} + +func (srv *TodoServer) UpdateTodo(ctx context.Context, req *connect.Request[todov1.UpdateTodoRequest]) (*connect.Response[todov1.UpdateTodoResponse], error) { + var id pgtype.UUID + err := id.Scan(*req.Msg.Todo.Id) + if err != nil { + return nil, err + } + todo, err := db.Q.UpdateTodo(ctx, db.UpdateTodoParams{ + Task: req.Msg.Todo.Task, + Done: pgtype.Bool{ + Bool: *req.Msg.Todo.Done, + Valid: true, + }, + ID: id, + }) + if err != nil { + return nil, err + } + return &connect.Response[todov1.UpdateTodoResponse]{ + Msg: &todov1.UpdateTodoResponse{ + Todo: &todov1.Todo{ + Id: StrPtr(todo.ID.String()), + Task: todo.Task, + CreatedAt: StrPtr(todo.CreatedAt.Time.Format(time.RFC3339)), + UpdatesAt: StrPtr(todo.UpdatedAt.Time.Format(time.RFC3339)), + Done: BoolPtr(todo.Done.Bool), + }, + }, + }, nil + +} + +func (srv *TodoServer) DeleteTodo(ctx context.Context, req *connect.Request[todov1.DeleteTodoRequest]) (*connect.Response[todov1.DeleteTodoResponse], error) { + var id pgtype.UUID + err := id.Scan(*req.Msg.Todo.Id) + if err != nil { + return nil, err + } + err = db.Q.DeleteTodo(ctx, id) + if err != nil { + return nil, err + } + return &connect.Response[todov1.DeleteTodoResponse]{ + Msg: &todov1.DeleteTodoResponse{}, + }, nil +} + +func GetPathHandler() (path string, handler http.Handler) { + path, handler = todov1connect.NewTodoServiceHandler(&TodoServer{}, connect.WithInterceptors(validate.NewInterceptor())) + return +} diff --git a/template/services/api/sqlc.yaml b/template/services/api/sqlc.yaml new file mode 100644 index 0000000..6e40552 --- /dev/null +++ b/template/services/api/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "db/query" + schema: "db/migrations" + gen: + go: + package: "db" + out: "db" + sql_package: "pgx/v5" diff --git a/template/services/api/utils/utils.go b/template/services/api/utils/utils.go new file mode 100644 index 0000000..8954b4b --- /dev/null +++ b/template/services/api/utils/utils.go @@ -0,0 +1,9 @@ +package utils + +func StrPtr(v string) *string { + return &v +} + +func BoolPtr(v bool) *bool { + return &v +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}