init go project

This commit is contained in:
juancwu 2025-10-10 08:14:33 -04:00
commit 5dde43e409
85 changed files with 16720 additions and 0 deletions

61
.gitignore vendored Normal file
View file

@ -0,0 +1,61 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Go workspace file
go.work
# Dependency directories
vendor/
node_modules/
# Build output
build/
bin/
tmp/
dist/
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# Environment variables
.env
.env.local
.env.*.local
.env.*
!.env.include.*
# Logs
*.log
logs/
# Air temporary files
tmp/
# Generated files
*_templ.go
*_templ.txt
# Database files
*.sqlite
*.sqlite3
/data/
*.db
*.db-shm
*.db-wal
output.css

7
.templui.json Normal file
View file

@ -0,0 +1,7 @@
{
"componentsDir": "internal/ui/components",
"utilsDir": "internal/utils",
"moduleName": "git.juancwu.dev/juancwu/budgething",
"jsDir": "assets/js",
"jsPublicPath": "/assets/js"
}

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

28
Taskfile.yml Normal file
View file

@ -0,0 +1,28 @@
version: "3"
tasks:
# Development Tools
templ:
desc: Run templ with integrated server and hot reload
cmds:
- go tool templ generate --watch --cmd="go run ./cmd/server/main.go" --proxy="http://localhost:9000" --open-browser=false
tailwind-clean:
desc: Clean tailwind output
cmds:
- tailwindcss -i ./assets/css/input.css -o ./assets/css/output.css --clean
tailwind-watch:
desc: Run tailwindcss in watch mode
cmds:
- tailwindcss -i ./assets/css/input.css -o ./assets/css/output.css --watch
# Start development server
dev:
desc: Start development server with hot reload
deps:
- tailwind-clean
cmds:
- echo "Starting app..."
- task --parallel tailwind-watch templ
# Stop all services
down:
desc: Stop all services
cmds:
- docker compose down

165
assets/css/input.css Normal file
View file

@ -0,0 +1,165 @@
@font-face {
font-family: "Geist";
src: url("/assets/fonts/geist/geist-variable.woff2")
format("woff2-variations");
font-weight: 100 900;
font-display: optional;
}
@font-face {
font-family: "Geist Mono";
src: url("/assets/fonts/geist/geist-mono-variable.woff2")
format("woff2-variations");
font-weight: 100 900;
font-display: optional;
}
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--breakpoint-3xl: 1600px;
--breakpoint-4xl: 2000px;
--font-sans:
"Geist", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono:
"Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Consolas,
"Liberation Mono", Menlo, monospace;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-surface: var(--surface);
--color-surface-foreground: var(--surface-foreground);
--color-code: var(--code);
--color-code-foreground: var(--code-foreground);
--color-code-highlight: var(--code-highlight);
--color-code-number: var(--code-number);
--color-selection: var(--selection);
--color-selection-foreground: var(--selection-foreground);
}
:root {
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(58.6% 0.253 17.585);
--primary-foreground: oklch(0.969 0.015 12.422);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.645 0.246 16.439);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(58.6% 0.253 17.585);
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
--sidebar-accent: oklch(0.94 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.645 0.246 16.439);
--selection: oklch(0.141 0.005 285.823);
--selection-foreground: oklch(1 0 0);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.645 0.246 16.439);
--primary-foreground: oklch(0.969 0.015 12.422);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.645 0.246 16.439);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.645 0.246 16.439);
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
--sidebar-accent: oklch(0.3 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.645 0.246 16.439);
--selection: oklch(0.92 0.004 286.32);
--selection-foreground: oklch(0.21 0.006 285.885);
}
@layer base {
* {
@apply border-border;
}
::selection {
@apply bg-selection text-selection-foreground;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
}

6
assets/embed.go Normal file
View file

@ -0,0 +1,6 @@
package assets
import "embed"
//go:embed js/* css/*
var AssetsFS embed.FS

1
assets/js/avatar.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";document.addEventListener("load",function(t){if(t.target.matches("[data-tui-avatar-image]")){let a=t.target.parentElement.querySelector("[data-tui-avatar-fallback]");a&&(a.style.display="none")}},!0),document.addEventListener("error",function(t){if(t.target.matches("[data-tui-avatar-image]")){t.target.style.display="none";let a=t.target.parentElement.querySelector("[data-tui-avatar-fallback]");a&&(a.style.display="flex")}},!0);function e(){document.querySelectorAll("[data-tui-avatar-image]").forEach(function(t){let a=t.parentElement.querySelector("[data-tui-avatar-fallback]");t.complete&&t.naturalWidth>0?a&&(a.style.display="none"):t.complete&&t.naturalWidth===0&&(t.style.display="none",a&&(a.style.display="flex"))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()})();})();

1
assets/js/calendar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
assets/js/carousel.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";let f=new Map,o=null;document.addEventListener("click",t=>{let e=t.target.closest("[data-tui-carousel-prev]");if(e){let s=e.closest("[data-tui-carousel]");s&&u(s,-1);return}let a=t.target.closest("[data-tui-carousel-next]");if(a){let s=a.closest("[data-tui-carousel]");s&&u(s,1);return}let r=t.target.closest("[data-tui-carousel-indicator]");if(r){let s=r.closest("[data-tui-carousel]"),n=parseInt(r.dataset.tuiCarouselIndicator);s&&!isNaN(n)&&c(s,n)}});function y(t){let e=t.target.closest("[data-tui-carousel-track]");if(!e)return;let a=e.closest("[data-tui-carousel]");if(!a)return;t.preventDefault();let r=t.touches?t.touches[0].clientX:t.clientX;o={carousel:a,track:e,startX:r,currentX:r,startTime:Date.now()},e.style.cursor="grabbing",e.style.transition="none",i(a)}function C(t){if(!o)return;let e=t.touches?t.touches[0].clientX:t.clientX;o.currentX=e;let a=e-o.startX,s=-parseInt(o.carousel.dataset.tuiCarouselCurrent||"0")*100+a/o.track.offsetWidth*100;o.track.style.transform=`translateX(${s}%)`}function m(t){if(!o)return;let{carousel:e,track:a,startX:r,startTime:s}=o,n=t.changedTouches?t.changedTouches[0].clientX:t.clientX||o.currentX;a.style.cursor="",a.style.transition="";let l=r-n,g=Math.abs(l)/(Date.now()-s);if(Math.abs(l)>50||g>.5)u(e,l>0?1:-1);else{let d=parseInt(e.dataset.tuiCarouselCurrent||"0");c(e,d)}o=null,e.dataset.tuiCarouselAutoplay==="true"&&!e.matches(":hover")&&b(e)}document.addEventListener("mousedown",y),document.addEventListener("mousemove",C),document.addEventListener("mouseup",m),document.addEventListener("mouseleave",t=>{t.target===document.documentElement&&m(t)}),document.addEventListener("touchstart",y,{passive:!1}),document.addEventListener("touchmove",C,{passive:!1}),document.addEventListener("touchend",m,{passive:!1});function u(t,e){let a=parseInt(t.dataset.tuiCarouselCurrent||"0"),s=t.querySelectorAll("[data-tui-carousel-item]").length;if(s===0)return;let n=a+e;t.dataset.tuiCarouselLoop==="true"?n=(n%s+s)%s:n=Math.max(0,Math.min(n,s-1)),c(t,n)}function c(t,e){let a=t.querySelector("[data-tui-carousel-track]"),r=t.querySelectorAll("[data-tui-carousel-indicator]"),s=t.querySelector("[data-tui-carousel-prev]"),n=t.querySelector("[data-tui-carousel-next]"),g=t.querySelectorAll("[data-tui-carousel-item]").length;t.dataset.tuiCarouselCurrent=e,a&&(a.style.transform=`translateX(-${e*100}%)`),r.forEach((p,h)=>{p.dataset.tuiCarouselActive=h===e?"true":"false",p.classList.toggle("bg-primary",h===e),p.classList.toggle("bg-foreground/30",h!==e)});let d=t.dataset.tuiCarouselLoop==="true";s&&(s.disabled=!d&&e===0,s.classList.toggle("opacity-50",s.disabled)),n&&(n.disabled=!d&&e===g-1,n.classList.toggle("opacity-50",n.disabled))}function b(t){if(t.dataset.tuiCarouselAutoplay!=="true")return;i(t);let e=parseInt(t.dataset.tuiCarouselInterval||"5000"),a=setInterval(()=>{if(!document.contains(t)){i(t);return}t.matches(":hover")||o?.carousel===t||u(t,1)},e);f.set(t,a)}function i(t){let e=f.get(t);e&&(clearInterval(e),f.delete(t))}let L=new WeakSet,X=new IntersectionObserver(t=>{t.forEach(e=>{let a=e.target;if(!a.hasAttribute("data-tui-carousel-initialized")){a.setAttribute("data-tui-carousel-initialized","true");let r=parseInt(a.dataset.tuiCarouselCurrent||"0");c(a,r)}a.dataset.tuiCarouselAutoplay==="true"&&(e.isIntersecting?b(a):i(a))})});function v(){document.querySelectorAll("[data-tui-carousel]").forEach(t=>{L.has(t)||(L.add(t),X.observe(t))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",v):v(),new MutationObserver(v).observe(document.body,{childList:!0,subtree:!0})})();})();

15
assets/js/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
assets/js/code.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";let e={light:"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css",dark:"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"};function s(){let t=document.documentElement.classList.contains("dark");document.querySelectorAll("#highlight-theme").forEach(l=>{l.href=t?e.dark:e.light})}function o(){window.hljs&&document.querySelectorAll("[data-tui-code-block]:not(.hljs)").forEach(t=>window.hljs.highlightElement(t))}function i(){s(),o()}function n(t){window.hljs?t():requestAnimationFrame(()=>n(t))}n(i),new MutationObserver(i).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0})})();})();

1
assets/js/collapsible.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function i(t){let e=t.closest('[data-tui-collapsible="root"]');if(!e)return;let a=e.getAttribute("data-tui-collapsible-state")==="open",n=a?"closed":"open";e.setAttribute("data-tui-collapsible-state",n),t.setAttribute("aria-expanded",!a)}document.addEventListener("click",t=>{let e=t.target.closest('[data-tui-collapsible="trigger"]');e&&(t.preventDefault(),i(e))}),document.addEventListener("keydown",t=>{if(t.key!==" "&&t.key!=="Enter")return;let e=t.target.closest('[data-tui-collapsible="trigger"]');e&&(t.preventDefault(),i(e))})})();})();

1
assets/js/copybutton.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";document.addEventListener("click",i=>{let e=i.target.closest("[data-copy-button]");if(!e)return;let t=e.dataset.targetId;if(!t){console.error("CopyButton: No target-id specified");return}let o=document.getElementById(t);if(!o){console.error(`CopyButton: Element with id '${t}' not found`);return}let c="";o.value!==void 0?c=o.value:c=o.textContent||"";let l=e.querySelector("[data-copy-icon-clipboard]"),r=e.querySelector("[data-copy-icon-check]");if(!l||!r)return;let a=()=>{l.style.display="none",r.style.display="inline";let n=e.closest(".inline-block")?.parentElement?.parentElement?.querySelector("[data-copy-tooltip-text]"),d=n?.textContent;n&&(n.textContent="Copied!"),setTimeout(()=>{l.style.display="inline",r.style.display="none",n&&d&&(n.textContent=d)},2e3)};navigator.clipboard&&window.isSecureContext?navigator.clipboard.writeText(c.trim()).then(a).catch(n=>{console.error("CopyButton: Failed to copy text",n),s(c.trim(),a)}):s(c.trim(),a)});function s(i,e){let t=document.createElement("textarea");t.value=i,t.style.position="fixed",t.style.top="-9999px",t.style.left="-9999px",document.body.appendChild(t),t.focus(),t.select();try{document.execCommand("copy")?e():console.error("CopyButton: Fallback copy failed")}catch(o){console.error("CopyButton: Fallback copy error",o)}document.body.removeChild(t)}})();})();

1
assets/js/datepicker.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function s(t){if(!t)return null;let e=t.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(!e)return null;let a=parseInt(e[1],10),n=parseInt(e[2],10)-1,d=parseInt(e[3],10),r=new Date(Date.UTC(a,n,d));return r.getUTCFullYear()===a&&r.getUTCMonth()===n&&r.getUTCDate()===d?r:null}function l(t,e,a){if(!t||isNaN(t.getTime()))return"";let n={timeZone:"UTC"},d={"locale-short":"short","locale-long":"long","locale-full":"full","locale-medium":"medium"};n.dateStyle=d[e]||"medium";try{return new Intl.DateTimeFormat(a,n).format(t)}catch{let i=t.getUTCFullYear(),c=(t.getUTCMonth()+1).toString().padStart(2,"0"),p=t.getUTCDate().toString().padStart(2,"0");return`${i}-${c}-${p}`}}function o(t){let e=t.id+"-calendar-instance",a=document.getElementById(e),n=document.getElementById(t.id+"-hidden")||t.parentElement?.querySelector("[data-tui-datepicker-hidden-input]"),d=t.querySelector("[data-tui-datepicker-display]");return{calendar:a,hiddenInput:n,display:d}}function u(t){let e=o(t);if(!e.display||!e.hiddenInput)return;let a=t.getAttribute("data-tui-datepicker-display-format")||"locale-medium",n=t.getAttribute("data-tui-datepicker-locale-tag")||"en-US",d=t.getAttribute("data-tui-datepicker-placeholder")||"Select a date";if(e.hiddenInput.value){let r=s(e.hiddenInput.value);if(r){e.display.textContent=l(r,a,n),e.display.classList.remove("text-muted-foreground");return}}e.display.textContent=d,e.display.classList.add("text-muted-foreground")}document.addEventListener("calendar-date-selected",t=>{let e=t.target;if(!e||!e.id.endsWith("-calendar-instance"))return;let a=e.id.replace("-calendar-instance",""),n=document.getElementById(a);if(!n||!n.hasAttribute("data-tui-datepicker"))return;let d=o(n);if(!d.display||!t.detail?.date)return;let r=n.getAttribute("data-tui-datepicker-display-format")||"locale-medium",i=n.getAttribute("data-tui-datepicker-locale-tag")||"en-US";if(d.display.textContent=l(t.detail.date,r,i),d.display.classList.remove("text-muted-foreground"),window.closePopover){let c=n.getAttribute("aria-controls")||n.id+"-content";window.closePopover(c)}}),document.addEventListener("reset",t=>{t.target.matches("form")&&t.target.querySelectorAll('[data-tui-datepicker="true"]').forEach(e=>{let a=o(e);a.hiddenInput&&(a.hiddenInput.value=""),u(e)})}),new MutationObserver(()=>{document.querySelectorAll('[data-tui-datepicker="true"]:not([data-rendered])').forEach(t=>{t.setAttribute("data-rendered","true"),u(t)})}).observe(document.body,{childList:!0,subtree:!0})})();})();

1
assets/js/dialog.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function u(t){let a=document.querySelector(`[data-tui-dialog-backdrop][data-dialog-instance="${t}"]`),e=document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${t}"]`);!a||!e||(a.removeAttribute("data-tui-dialog-hidden"),e.removeAttribute("data-tui-dialog-hidden"),requestAnimationFrame(()=>{a.setAttribute("data-tui-dialog-open","true"),e.setAttribute("data-tui-dialog-open","true"),document.body.style.overflow="hidden",document.querySelectorAll(`[data-tui-dialog-trigger][data-dialog-instance="${t}"]`).forEach(o=>{o.setAttribute("data-tui-dialog-trigger-open","true")}),e.hasAttribute("data-tui-dialog-disable-autofocus")||setTimeout(()=>{e.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')?.focus()},50)}))}function n(t){let a=document.querySelector(`[data-tui-dialog-backdrop][data-dialog-instance="${t}"]`),e=document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${t}"]`);!a||!e||(a.setAttribute("data-tui-dialog-open","false"),e.setAttribute("data-tui-dialog-open","false"),document.querySelectorAll(`[data-tui-dialog-trigger][data-dialog-instance="${t}"]`).forEach(i=>{i.setAttribute("data-tui-dialog-trigger-open","false")}),setTimeout(()=>{a.setAttribute("data-tui-dialog-hidden","true"),e.setAttribute("data-tui-dialog-hidden","true"),document.querySelector('[data-tui-dialog-content][data-tui-dialog-open="true"]')||(document.body.style.overflow="")},300))}function l(t){let a=t.getAttribute("data-dialog-instance");if(a)return a;let e=t.closest("[data-tui-dialog]");return e?e.getAttribute("data-dialog-instance"):null}function r(t){return document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${t}"]`)?.getAttribute("data-tui-dialog-open")==="true"||!1}function c(t){r(t)?n(t):u(t)}document.addEventListener("click",t=>{let a=t.target.closest("[data-tui-dialog-trigger]");if(a){let o=a.getAttribute("data-dialog-instance");if(!o)return;c(o);return}let e=t.target.closest("[data-tui-dialog-close]");if(e){let d=e.getAttribute("data-tui-dialog-close")||l(e);d&&n(d);return}let i=t.target.closest("[data-tui-dialog-backdrop]");if(i){let o=i.getAttribute("data-dialog-instance");if(!o)return;let d=document.querySelector(`[data-tui-dialog][data-dialog-instance="${o}"]`),s=document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${o}"]`);d?.hasAttribute("data-tui-dialog-disable-click-away")||s?.hasAttribute("data-tui-dialog-disable-click-away")||n(o)}}),document.addEventListener("keydown",t=>{if(t.key==="Escape"){let a=document.querySelectorAll('[data-tui-dialog-content][data-tui-dialog-open="true"]');if(a.length===0)return;let e=a[a.length-1],i=e.getAttribute("data-dialog-instance");if(!i)return;document.querySelector(`[data-tui-dialog][data-dialog-instance="${i}"]`)?.hasAttribute("data-tui-dialog-disable-esc")||e?.hasAttribute("data-tui-dialog-disable-esc")||n(i)}}),document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll('[data-tui-dialog-content][data-tui-dialog-open="true"]').length>0&&(document.body.style.overflow="hidden")}),new MutationObserver(()=>{document.querySelector('[data-tui-dialog-content][data-tui-dialog-open="true"]')||(document.body.style.overflow="")}).observe(document.body,{childList:!0,subtree:!0}),window.tui=window.tui||{},window.tui.dialog={open:u,close:n,toggle:c,isOpen:r}})();})();

1
assets/js/dropdown.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";document.addEventListener("click",o=>{let t=o.target.closest("[data-tui-dropdown-item]");if(!t||t.hasAttribute("data-tui-dropdown-submenu-trigger")||t.getAttribute("data-tui-dropdown-prevent-close")==="true")return;let e=t.closest("[data-tui-popover-id]");if(!e)return;let i=e.getAttribute("data-tui-popover-id")||e.id;window.closePopover&&window.closePopover(i)})})();})();

1
assets/js/input.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";document.addEventListener("click",n=>{let t=n.target.closest("[data-tui-input-toggle-password]");if(!t)return;let o=t.getAttribute("data-tui-input-toggle-password"),e=document.getElementById(o);if(!e)return;let s=t.querySelector(".icon-open"),i=t.querySelector(".icon-closed");e.type==="password"?(e.type="text",s&&s.classList.add("hidden"),i&&i.classList.remove("hidden")):(e.type="password",s&&s.classList.remove("hidden"),i&&i.classList.add("hidden"))})})();})();

1
assets/js/inputotp.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function u(e){return Array.from(e.querySelectorAll("[data-tui-inputotp-slot]")).sort((t,n)=>parseInt(t.getAttribute("data-tui-inputotp-index"))-parseInt(n.getAttribute("data-tui-inputotp-index")))}function a(e){e&&(e.focus(),setTimeout(()=>e.select(),0))}function i(e){let t=e.querySelector("[data-tui-inputotp-value-target]"),n=u(e);t&&n.length&&(t.value=n.map(o=>o.value).join(""))}function d(e){let t=u(e);for(let n of t)if(!n.value)return n;return null}function f(e,t){let n=u(e),o=n.indexOf(t);return o>=0&&o<n.length-1?n[o+1]:null}function p(e,t){let n=u(e),o=n.indexOf(t);return o>0?n[o-1]:null}document.addEventListener("input",e=>{if(!e.target.matches("[data-tui-inputotp-slot]"))return;let t=e.target,n=t.closest("[data-tui-inputotp]");if(n){if(t.value===" "){t.value="";return}if(t.value.length>1&&(t.value=t.value.slice(-1)),t.value){let o=f(n,t);o&&a(o)}i(n)}}),document.addEventListener("keydown",e=>{if(!e.target.matches("[data-tui-inputotp-slot]"))return;let t=e.target,n=t.closest("[data-tui-inputotp]");if(n){if(e.key==="Backspace")if(e.preventDefault(),t.value)t.value="",i(n);else{let o=p(n,t);o&&(o.value="",i(n),a(o))}else if(e.key==="ArrowLeft"){e.preventDefault();let o=p(n,t);o&&a(o)}else if(e.key==="ArrowRight"){e.preventDefault();let o=f(n,t);o&&a(o)}}}),document.addEventListener("focus",e=>{if(!e.target.matches("[data-tui-inputotp-slot]"))return;let t=e.target,n=t.closest("[data-tui-inputotp]");if(!n)return;let o=d(n);if(o&&o!==t){a(o);return}setTimeout(()=>t.select(),0)},!0),document.addEventListener("paste",e=>{let t=e.target.closest("[data-tui-inputotp-slot]");if(!t)return;e.preventDefault();let n=t.closest("[data-tui-inputotp]");if(!n)return;let r=(e.clipboardData||window.clipboardData).getData("text").replace(/\s/g,"").split(""),s=u(n),c=s.indexOf(t);for(let l=0;l<r.length&&c+l<s.length;l++)s[c+l].value=r[l];i(n);let v=d(n);a(v||s[Math.min(c+r.length,s.length-1)])}),document.addEventListener("click",e=>{if(!e.target.matches("label[for]"))return;let t=e.target.getAttribute("for"),n=document.getElementById(t);if(!n?.matches("[data-tui-inputotp-value-target]"))return;e.preventDefault();let o=n.closest("[data-tui-inputotp]"),r=u(o);r.length>0&&a(r[0])}),document.addEventListener("reset",e=>{e.target.matches("form")&&e.target.querySelectorAll("[data-tui-inputotp]").forEach(t=>{u(t).forEach(n=>{n.value=""}),i(t)})}),new MutationObserver(()=>{document.querySelectorAll("[data-tui-inputotp]").forEach(e=>{let t=u(e);if(t.length===0)return;let n=e.getAttribute("data-tui-inputotp-value");if(n&&!t[0].value){for(let o=0;o<t.length&&o<n.length;o++)t[o].value||(t[o].value=n[o]);i(e)}e.hasAttribute("autofocus")&&!t.some(o=>o===document.activeElement)&&requestAnimationFrame(()=>{t[0]&&!t.some(o=>o===document.activeElement)&&a(t[0])})})}).observe(document.body,{childList:!0,subtree:!0})})();})();

1
assets/js/label.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function a(t){let s=t.getAttribute("for"),d=s?document.getElementById(s):null,r=t.getAttribute("data-tui-label-disabled-style");if(!d||!r)return;let e=r.split(" ").filter(Boolean);d.disabled?t.classList.add(...e):t.classList.remove(...e)}document.addEventListener("DOMContentLoaded",()=>{let t=new Set;function s(){document.querySelectorAll("label[for][data-tui-label-disabled-style]").forEach(r=>{a(r);let e=r.getAttribute("for");e&&t.add(e)})}s(),new MutationObserver(r=>{r.forEach(e=>{e.type==="attributes"&&e.attributeName==="disabled"&&e.target.id&&t.has(e.target.id)&&document.querySelectorAll(`label[for="${e.target.id}"][data-tui-label-disabled-style]`).forEach(a)})}).observe(document.body,{attributes:!0,attributeFilter:["disabled"],subtree:!0}),new MutationObserver(()=>{s()}).observe(document.body,{childList:!0,subtree:!0})})})();})();

6
assets/js/popover.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
assets/js/progress.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function a(r){let e=r.querySelector("[data-tui-progress-indicator]");if(!e)return;let t=parseFloat(r.getAttribute("aria-valuenow")||"0"),u=parseFloat(r.getAttribute("aria-valuemax")||"100")||100,i=Math.max(0,Math.min(100,t/u*100));e.style.width=i+"%"}function o(){document.querySelectorAll('[role="progressbar"]').forEach(a)}document.addEventListener("DOMContentLoaded",()=>{o();let r=new MutationObserver(e=>{e.forEach(t=>{t.type==="attributes"&&(t.attributeName==="aria-valuenow"||t.attributeName==="aria-valuemax")&&a(t.target)})});new MutationObserver(()=>{document.querySelectorAll('[role="progressbar"]').forEach(e=>{e.hasAttribute("data-tui-progress-observed")||(e.setAttribute("data-tui-progress-observed","true"),a(e),r.observe(e,{attributes:!0,attributeFilter:["aria-valuenow","aria-valuemax"]}))})}).observe(document.body,{childList:!0,subtree:!0})})})();})();

1
assets/js/rating.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function u(t){return{value:parseFloat(t.getAttribute("data-tui-rating-initial-value"))||0,precision:parseFloat(t.getAttribute("data-tui-rating-precision"))||1,readonly:t.getAttribute("data-tui-rating-readonly")==="true",name:t.getAttribute("data-tui-rating-name")||"",onlyInteger:t.getAttribute("data-tui-rating-onlyinteger")==="true"}}function d(t){return parseFloat(t.getAttribute("data-tui-rating-current"))||parseFloat(t.getAttribute("data-tui-rating-initial-value"))||0}function l(t,a){t.setAttribute("data-tui-rating-current",a);let e=t.querySelector("[data-tui-rating-input]");e&&(e.value=a.toFixed(2),e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0})))}function c(t,a){let e=d(t),r=a>0?a:e;t.querySelectorAll("[data-tui-rating-item]").forEach(i=>{let o=parseInt(i.getAttribute("data-tui-rating-value"),10);if(isNaN(o))return;let s=i.querySelector("[data-tui-rating-item-foreground]");if(!s)return;let n=o<=Math.floor(r),f=!n&&o-1<r&&r<o,p=f?(r-Math.floor(r))*100:0;s.style.width=n?"100%":f?`${p}%`:"0%"})}function g(t){let a=0;return t.querySelectorAll("[data-tui-rating-item]").forEach(e=>{let r=parseInt(e.getAttribute("data-tui-rating-value"),10);!isNaN(r)&&r>a&&(a=r)}),Math.max(1,a)}document.addEventListener("click",t=>{let a=t.target.closest("[data-tui-rating-item]");if(!a)return;let e=a.closest("[data-tui-rating-component]");if(!e)return;let r=u(e);if(r.readonly)return;let i=parseInt(a.getAttribute("data-tui-rating-value"),10);if(isNaN(i))return;let o=d(e),s=g(e),n=i;r.onlyInteger?n=Math.round(n):o===n&&n%1===0?n=Math.max(0,n-r.precision):n=Math.round(n/r.precision)*r.precision,n=Math.max(0,Math.min(s,n)),l(e,n),c(e,0),e.dispatchEvent(new CustomEvent("rating-change",{bubbles:!0,detail:{name:r.name,value:n,maxValue:s}}))}),document.addEventListener("mouseover",t=>{let a=t.target.closest("[data-tui-rating-item]");if(!a)return;let e=a.closest("[data-tui-rating-component]");if(!e||u(e).readonly)return;let r=parseInt(a.getAttribute("data-tui-rating-value"),10);isNaN(r)||c(e,r)}),document.addEventListener("mouseout",t=>{let a=t.target.closest("[data-tui-rating-component]");!a||u(a).readonly||a.contains(t.relatedTarget)||c(a,0)}),document.addEventListener("reset",t=>{t.target.matches("form")&&t.target.querySelectorAll("[data-tui-rating-component]").forEach(a=>{let e=u(a);l(a,e.value),c(a,0)})}),new MutationObserver(()=>{document.querySelectorAll("[data-tui-rating-component]").forEach(t=>{if(!t.hasAttribute("data-tui-rating-current")){let e=u(t),r=g(t),i=Math.max(0,Math.min(r,e.value));l(t,Math.round(i/e.precision)*e.precision)}c(t,0),u(t).readonly&&(t.style.cursor="default",t.querySelectorAll("[data-tui-rating-item]").forEach(e=>{e.style.cursor="default"}))})}).observe(document.body,{childList:!0,subtree:!0})})();})();

1
assets/js/selectbox.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
assets/js/sidebar.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";let n="sidebar_state";function d(){document.querySelectorAll("[data-tui-sidebar-content]").forEach(e=>{let a=e.getAttribute("data-tui-sidebar-content"),r=document.querySelector(`[data-tui-sidebar-mobile-portal="${a}"]`);if(!r)return;let i=window.matchMedia("(max-width: 767px)").matches;if(i&&e.parentElement!==r)r.appendChild(e);else if(!i&&e.parentElement===r){let s=document.querySelector(`[data-tui-sidebar-wrapper][data-tui-sidebar-id="${a}"] [data-sidebar="sidebar"] > div`);s&&s.appendChild(e)}})}d(),window.addEventListener("resize",d),new MutationObserver(d).observe(document.body,{childList:!0,subtree:!0}),document.addEventListener("click",t=>{let e=t.target.closest("[data-tui-sidebar-trigger]");if(e){t.preventDefault();let a=e.getAttribute("data-tui-sidebar-target");a&&o(a)}}),document.addEventListener("keydown",t=>{if((t.ctrlKey||t.metaKey)&&t.key.length===1){let e=document.querySelector("[data-tui-sidebar-wrapper]");if(!e)return;let a=e.getAttribute("data-tui-sidebar-keyboard-shortcut");if(!a||a.toLowerCase()!==t.key.toLowerCase())return;t.preventDefault();let r=e.querySelector('[data-sidebar="sidebar"]');r&&r.id&&o(r.id)}});function o(t){let e=document.querySelector(`[data-tui-sidebar-wrapper][data-tui-sidebar-id="${t}"]`);if(!e||e.getAttribute("data-tui-sidebar-collapsible")==="none")return;let i=e.getAttribute("data-tui-sidebar-state")==="expanded"?"collapsed":"expanded";u(t,i)}function u(t,e){let a=document.querySelector(`[data-tui-sidebar-wrapper][data-tui-sidebar-id="${t}"]`);if(!a)return;let r=a.getAttribute("data-tui-sidebar-collapsible");if(r==="none")return;a.setAttribute("data-tui-sidebar-state",e),e==="collapsed"&&r&&a.setAttribute("data-tui-sidebar-collapsible",r),c(t,e==="expanded"?"true":"false")}function c(t,e){document.cookie=`${n}=${e}; path=/; max-age=604800`}})();})();

1
assets/js/slider.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";document.addEventListener("input",e=>{let t=e.target.closest('input[type="range"][data-tui-slider-input]');!t||!t.id||document.querySelectorAll(`[data-tui-slider-value][data-tui-slider-value-for="${t.id}"]`).forEach(u=>{u.textContent=t.value})}),new MutationObserver(()=>{document.querySelectorAll('input[type="range"][data-tui-slider-input]').forEach(e=>{e.id&&document.querySelectorAll(`[data-tui-slider-value][data-tui-slider-value-for="${e.id}"]`).forEach(t=>{(!t.textContent||t.textContent==="")&&(t.textContent=e.value)})})}).observe(document.body,{childList:!0,subtree:!0})})();})();

1
assets/js/tabs.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function u(a,e){document.querySelectorAll(`[data-tui-tabs-trigger][data-tui-tabs-id="${a}"]`).forEach(t=>{let i=t.getAttribute("data-tui-tabs-value")===e;t.setAttribute("data-tui-tabs-state",i?"active":"inactive")}),document.querySelectorAll(`[data-tui-tabs-content][data-tui-tabs-id="${a}"]`).forEach(t=>{let i=t.getAttribute("data-tui-tabs-value")===e;t.setAttribute("data-tui-tabs-state",i?"active":"inactive"),t.classList.toggle("hidden",!i)})}document.addEventListener("click",a=>{let e=a.target.closest("[data-tui-tabs-trigger]");if(!e)return;let t=e.getAttribute("data-tui-tabs-id"),i=e.getAttribute("data-tui-tabs-value");t&&i&&u(t,i)});function s(){document.querySelectorAll("[data-tui-tabs]").forEach(a=>{let e=a.getAttribute("data-tui-tabs-id");if(!e)return;let t=a.querySelector('[data-tui-tabs-trigger][data-tui-tabs-state="active"]')||a.querySelector("[data-tui-tabs-trigger]");t&&u(e,t.getAttribute("data-tui-tabs-value"))})}document.addEventListener("DOMContentLoaded",s),new MutationObserver(s).observe(document.body,{childList:!0,subtree:!0}),window.tui=window.tui||{},window.tui.tabs={setActive:u}})();})();

11
assets/js/tagsinput.min.js vendored Normal file
View file

@ -0,0 +1,11 @@
(()=>{(function(){"use strict";function d(t,e){let n=document.createElement("div");return n.setAttribute("data-tui-tagsinput-chip",""),n.className="inline-flex items-center gap-2 rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground",n.innerHTML=`
<span>${t}</span>
<button type="button"
class="ml-1 text-current hover:text-destructive disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
data-tui-tagsinput-remove=""
${e?"disabled":""}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
`,n}function c(t,e){let n=t.querySelector("[data-tui-tagsinput-text-input]");if(n?.hasAttribute("disabled"))return;let i=e.trim();if(!i)return;let a=t.querySelector("[data-tui-tagsinput-hidden-inputs]"),u=t.querySelector("[data-tui-tagsinput-container]"),p=t.getAttribute("data-tui-tagsinput-name"),o=t.getAttribute("data-tui-tagsinput-form"),l=a.querySelectorAll('input[type="hidden"]');for(let f of l)if(f.value.toLowerCase()===i.toLowerCase()){n.value="";return}let g=d(i,n?.hasAttribute("disabled"));u.appendChild(g);let r=document.createElement("input");r.type="hidden",r.name=p,r.value=i,o!==null&&o!==""&&r.setAttribute("form",o),a.appendChild(r),n.value=""}function s(t){let e=t.closest("[data-tui-tagsinput-chip]");if(!e)return;let n=e.closest("[data-tui-tagsinput]"),i=e.querySelector("span").textContent.trim(),u=n.querySelector("[data-tui-tagsinput-hidden-inputs]").querySelector(`input[type="hidden"][value="${i}"]`);u&&u.remove(),e.remove()}document.addEventListener("keydown",t=>{let e=t.target.closest("[data-tui-tagsinput-text-input]");if(!e)return;let n=e.closest("[data-tui-tagsinput]");if(n){if(t.key==="Enter"||t.key===",")t.preventDefault(),c(n,e.value);else if(t.key==="Backspace"&&e.value===""){t.preventDefault();let a=n.querySelector("[data-tui-tagsinput-chip]:last-child")?.querySelector("[data-tui-tagsinput-remove]");a&&!a.disabled&&s(a)}}}),document.addEventListener("click",t=>{let e=t.target.closest("[data-tui-tagsinput-remove]");if(e&&!e.disabled){t.preventDefault(),t.stopPropagation(),s(e);return}let n=t.target.closest("[data-tui-tagsinput]");if(n&&!t.target.closest("input")){let i=n.querySelector("[data-tui-tagsinput-text-input]");i&&i.focus()}}),document.addEventListener("reset",t=>{t.target.matches("form")&&t.target.querySelectorAll("[data-tui-tagsinput]").forEach(e=>{e.querySelectorAll("[data-tui-tagsinput-chip]").forEach(i=>i.remove()),e.querySelectorAll('[data-tui-tagsinput-hidden-inputs] input[type="hidden"]').forEach(i=>i.remove());let n=e.querySelector("[data-tui-tagsinput-text-input]");n&&(n.value="")})})})();})();

1
assets/js/textarea.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";document.addEventListener("input",t=>{let e=t.target.closest("textarea[data-tui-textarea]");if(!e||e.getAttribute("data-tui-textarea-auto-resize")!=="true")return;let i=e.style.minHeight||window.getComputedStyle(e).minHeight;e.style.height=i,e.style.height=`${e.scrollHeight}px`}),new MutationObserver(()=>{document.querySelectorAll('textarea[data-tui-textarea][data-tui-textarea-auto-resize="true"]').forEach(t=>{(!t.style.height||t.style.height===t.style.minHeight)&&(t.style.height=`${t.scrollHeight}px`)})}).observe(document.body,{childList:!0,subtree:!0})})();})();

1
assets/js/timepicker.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";function m(i){let t=i?.match(/^(\d{1,2}):(\d{2})$/);if(!t)return null;let[u,e,r]=t.map(Number);return e>=0&&e<=23&&r>=0&&r<=59?{hour:e,minute:r}:null}function h(i,t,u){if(i===null||t===null)return null;let e=r=>r.toString().padStart(2,"0");if(u){let r=i===0?12:i>12?i-12:i;return`${e(r)}:${e(t)} ${i>=12?"PM":"AM"}`}return`${e(i)}:${e(t)}`}function c(i,t,u,e){if(!u&&!e)return!0;let r=i*60+t;if(u){let n=u.hour*60+u.minute;if(r<n)return!1}if(e){let n=e.hour*60+e.minute;if(r>n)return!1}return!0}function s(i){let t=i.closest("[data-tui-timepicker-popup]");if(!t)return null;let u=t.closest("[id]")?.id;return u?document.getElementById(u.replace("-content","")):null}function p(i){let t=i.id+"-content",u=document.getElementById(t)?.querySelector("[data-tui-timepicker-popup]");return u?{popup:u,hourList:u.querySelector("[data-tui-timepicker-hour-list]"),minuteList:u.querySelector("[data-tui-timepicker-minute-list]"),hiddenInput:document.getElementById(i.id+"-hidden")||i.parentElement?.querySelector("[data-tui-timepicker-hidden-input]")}:null}function d(i){return{hour:i.dataset.tuiTimepickerCurrentHour?parseInt(i.dataset.tuiTimepickerCurrentHour):null,minute:i.dataset.tuiTimepickerCurrentMinute?parseInt(i.dataset.tuiTimepickerCurrentMinute):null,use12Hours:i.getAttribute("data-tui-timepicker-use12hours")==="true",step:parseInt(i.getAttribute("data-tui-timepicker-step")||"1"),minTime:m(i.getAttribute("data-tui-timepicker-min-time")),maxTime:m(i.getAttribute("data-tui-timepicker-max-time")),placeholder:i.getAttribute("data-tui-timepicker-placeholder")||"Select time"}}function l(i,t,u){t!==null?i.dataset.tuiTimepickerCurrentHour=t:delete i.dataset.tuiTimepickerCurrentHour,u!==null?i.dataset.tuiTimepickerCurrentMinute=u:delete i.dataset.tuiTimepickerCurrentMinute,k(i)}function k(i){let t=d(i),u=p(i),e=i.querySelector("[data-tui-timepicker-display]");if(e){let r=h(t.hour,t.minute,t.use12Hours);e.textContent=r||t.placeholder,e.classList.toggle("text-muted-foreground",!r)}u?.hiddenInput&&(u.hiddenInput.value=t.hour!==null&&t.minute!==null?h(t.hour,t.minute,!1):""),u?.hourList&&u?.minuteList&&A(u,t)}function A(i,t){i.hourList.querySelectorAll("[data-tui-timepicker-hour]").forEach(r=>{let n=parseInt(r.getAttribute("data-tui-timepicker-hour")),o=!1;t.hour!==null&&(t.use12Hours?o=n===t.hour||n===0&&t.hour===12||n===t.hour-12&&t.hour>12:o=n===t.hour),r.setAttribute("data-tui-timepicker-selected",o);let a=!1;for(let f=0;f<60;f++)if(c(n,f,t.minTime,t.maxTime)){a=!0;break}r.disabled=!a,r.classList.toggle("opacity-50",!a),r.classList.toggle("cursor-not-allowed",!a)}),i.minuteList.querySelectorAll("[data-tui-timepicker-minute]").forEach(r=>{let n=parseInt(r.getAttribute("data-tui-timepicker-minute")),o=n===t.minute,a=t.hour===null||c(t.hour,n,t.minTime,t.maxTime);r.setAttribute("data-tui-timepicker-selected",o),r.disabled=!a,r.classList.toggle("opacity-50",!a),r.classList.toggle("cursor-not-allowed",!a)});let u=i.popup.querySelector('[data-tui-timepicker-period="AM"]'),e=i.popup.querySelector('[data-tui-timepicker-period="PM"]');if(u&&e){let r=t.hour===null||t.hour<12;u.setAttribute("data-tui-timepicker-active",r),e.setAttribute("data-tui-timepicker-active",!r)}}document.addEventListener("click",i=>{let t=i.target;if(t.matches("[data-tui-timepicker-hour]")&&!t.disabled){let u=s(t);if(!u)return;let e=d(u),r=parseInt(t.getAttribute("data-tui-timepicker-hour"));if(e.use12Hours){let n=e.hour!==null&&e.hour>=12;r=r===0?n?12:0:n?r+12:r}if(c(r,e.minute,e.minTime,e.maxTime))l(u,r,e.minute);else for(let n=0;n<60;n+=e.step)if(c(r,n,e.minTime,e.maxTime)){l(u,r,n);return}return}if(t.matches("[data-tui-timepicker-minute]")&&!t.disabled){let u=s(t);if(!u)return;let e=d(u),r=parseInt(t.getAttribute("data-tui-timepicker-minute"));(e.hour===null||c(e.hour,r,e.minTime,e.maxTime))&&l(u,e.hour,r);return}if(t.matches("[data-tui-timepicker-period]")){let u=s(t);if(!u)return;let e=d(u);if(e.hour===null)return;let r=t.getAttribute("data-tui-timepicker-period"),n=e.hour;if(r==="AM"&&e.hour>=12?n=e.hour===12?0:e.hour-12:r==="PM"&&e.hour<12&&(n=e.hour===0?12:e.hour+12),n!==e.hour){if(c(n,e.minute,e.minTime,e.maxTime))l(u,n,e.minute);else for(let o=0;o<60;o+=e.step)if(c(n,o,e.minTime,e.maxTime)){l(u,n,o);return}}return}if(t.matches("[data-tui-timepicker-done]")){let u=s(t);u&&window.closePopover&&window.closePopover(u.id+"-content");return}}),document.addEventListener("reset",i=>{i.target.matches("form")&&i.target.querySelectorAll('[data-tui-timepicker="true"]').forEach(t=>{l(t,null,null);let u=p(t);u?.hiddenInput&&(u.hiddenInput.value="")})}),new MutationObserver(()=>{document.querySelectorAll('[data-tui-timepicker="true"]:not([data-rendered])').forEach(i=>{i.setAttribute("data-rendered","true");let t=p(i),u=t?.hiddenInput?.value||t?.popup?.getAttribute("data-tui-timepicker-value");if(u){let e=m(u);e&&l(i,e.hour,e.minute)}k(i)})}).observe(document.body,{childList:!0,subtree:!0})})();})();

1
assets/js/toast.min.js vendored Normal file
View file

@ -0,0 +1 @@
(()=>{(function(){"use strict";let n=new Map;function o(e){let i=parseInt(e.dataset.tuiToastDuration||"3000"),t=e.querySelector(".toast-progress"),a={timer:null,startTime:Date.now(),remaining:i,paused:!1};n.set(e,a),t&&i>0&&(t.style.transitionDuration=i+"ms",requestAnimationFrame(()=>{requestAnimationFrame(()=>{t.style.transform="scaleX(0)"})})),i>0&&(a.timer=setTimeout(()=>r(e),i)),e.addEventListener("mouseenter",()=>{let s=n.get(e);if(!(!s||s.paused)&&(clearTimeout(s.timer),s.remaining=s.remaining-(Date.now()-s.startTime),s.paused=!0,t)){let m=getComputedStyle(t);t.style.transitionDuration="0ms",t.style.transform=m.transform}}),e.addEventListener("mouseleave",()=>{let s=n.get(e);!s||!s.paused||s.remaining<=0||(s.startTime=Date.now(),s.paused=!1,s.timer=setTimeout(()=>r(e),s.remaining),t&&(t.style.transitionDuration=s.remaining+"ms",requestAnimationFrame(()=>{requestAnimationFrame(()=>{t.style.transform="scaleX(0)"})})))})}function r(e){n.delete(e),e.style.transition="opacity 300ms, transform 300ms",e.style.opacity="0",e.style.transform="translateY(1rem)",setTimeout(()=>e.remove(),300)}document.addEventListener("click",e=>{let i=e.target.closest("[data-tui-toast-dismiss]");if(i){let t=i.closest("[data-tui-toast]");t&&r(t)}}),new MutationObserver(e=>{e.forEach(i=>{i.addedNodes.forEach(t=>{t.nodeType===1&&t.matches?.("[data-tui-toast]")&&o(t)})})}).observe(document.body,{childList:!0,subtree:!0})})();})();

36
cmd/server/main.go Normal file
View file

@ -0,0 +1,36 @@
package main
import (
"fmt"
"log/slog"
"net/http"
"git.juancwu.dev/juancwu/budgething/internal/app"
"git.juancwu.dev/juancwu/budgething/internal/config"
"git.juancwu.dev/juancwu/budgething/internal/routes"
)
func main() {
cfg := config.Load()
a, err := app.New(cfg)
if err != nil {
slog.Error("failed to initialize app", "error", err)
panic(err)
}
defer func() {
err := a.Close()
if err != nil {
slog.Error("failed to close app", "error", err)
}
}()
handler := routes.SetupRoutes(a)
slog.Info("server starting", "host", cfg.Host, "port", cfg.Port, "env", cfg.AppEnv, "url", fmt.Sprintf("http://%s:%s", cfg.Host, cfg.Port))
err = http.ListenAndServe(":"+cfg.Port, handler)
if err != nil {
slog.Error("server failed", "error", err)
panic(err)
}
}

88
go.mod Normal file
View file

@ -0,0 +1,88 @@
module git.juancwu.dev/juancwu/budgething
go 1.25.1
require (
github.com/Oudwins/tailwind-merge-go v0.2.1
github.com/a-h/templ v0.3.960
github.com/jackc/pgx/v5 v5.7.6
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.26.0
modernc.org/sqlite v1.40.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.67.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // indirect
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/go-sysinfo v1.15.4 // indirect
github.com/elastic/go-windows v1.0.2 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mfridman/xflag v0.1.0 // indirect
github.com/microsoft/go-mssqldb v1.9.2 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/templui/templui v0.101.0 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
github.com/vertica/vertica-sql-go v1.3.3 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // indirect
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
tool (
github.com/a-h/templ/cmd/templ
github.com/pressly/goose/v3/cmd/goose
github.com/templui/templui/cmd/templui
)

388
go.sum Normal file
View file

@ -0,0 +1,388 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc=
github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18=
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4=
github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc=
github.com/Oudwins/tailwind-merge-go v0.2.1 h1:jxRaEqGtwwwF48UuFIQ8g8XT7YSualNuGzCvQ89nPFE=
github.com/Oudwins/tailwind-merge-go v0.2.1/go.mod h1:kkZodgOPvZQ8f7SIrlWkG/w1g9JTbtnptnePIh3V72U=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
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.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mfridman/xflag v0.1.0 h1:TWZrZwG1QklFX5S4j1vxfF1sZbZeZSGofMwPMLAF29M=
github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE=
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=
github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/templui/templui v0.101.0 h1:Nv2WiyevFZ+6jtELRYxmVwHlu9WXXIyi6etvgP+tkbI=
github.com/templui/templui v0.101.0/go.mod h1:SnKmOIs7t/ngsdWUws97CVodbz89ne9kQv3ivgdhiHo=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY6cI8cP4B9rrpTleZk95+08kl2gF4rixG7+V/dwL6Q=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBMXp68hB5nNriUk=
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1/go.mod h1:l5sSv153E18VvYcsmr51hok9Sjc16tEC8AXGbwrk+ho=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

38
internal/app/app.go Normal file
View file

@ -0,0 +1,38 @@
package app
import (
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/config"
"git.juancwu.dev/juancwu/budgething/internal/db"
"github.com/jmoiron/sqlx"
)
type App struct {
Cfg *config.Config
DB *sqlx.DB
}
func New(cfg *config.Config) (*App, error) {
database, err := db.Init(cfg.DBDriver, cfg.DBConnection)
if err != nil {
return nil, fmt.Errorf("failed to initialize database: %w", err)
}
err = db.RunMigrations(database.DB, cfg.DBDriver)
if err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
}
return &App{
Cfg: cfg,
DB: database,
}, nil
}
func (a *App) Close() error {
if a.DB != nil {
return a.DB.Close()
}
return nil
}

76
internal/config/config.go Normal file
View file

@ -0,0 +1,76 @@
package config
import (
"log/slog"
"os"
"time"
"github.com/joho/godotenv"
)
type Config struct {
AppName string
AppEnv string
AppURL string
Host string
Port string
DBDriver string
DBConnection string
JWTSecret string
JWTExpiry time.Duration
}
func Load() *Config {
if err := godotenv.Load(); err != nil {
slog.Info("no .env file found, using environment variables")
}
cfg := &Config{
AppName: envString("APP_NAME", "Budgething"),
AppEnv: envRequired("APP_ENV"),
AppURL: envRequired("APP_URL"),
Host: envString("HOST", "127.0.0.1"),
Port: envString("PORT", "9000"),
DBDriver: envString("DB_DRIVER", "sqlite"),
DBConnection: envString("DB_CONNECTION", "./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)"),
JWTSecret: envRequired("JWT_SECRET"),
JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default
}
return cfg
}
func envString(key, def string) string {
value := os.Getenv(key)
if value == "" {
value = def
}
return value
}
func envDuration(key string, def time.Duration) time.Duration {
value, ok := os.LookupEnv(key)
if !ok || value == "" {
return def
}
duration, err := time.ParseDuration(value)
if err != nil {
slog.Warn("config invalid duration, using default", "key", key, "value", value, "default", def)
return def
}
return duration
}
func envRequired(key string) string {
if value := os.Getenv(key); value != "" {
return value
}
slog.Error("config required env var missing", "key", key)
os.Exit(1)
return ""
}

48
internal/db/db.go Normal file
View file

@ -0,0 +1,48 @@
package db
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
)
func Init(driver, connection string) (*sqlx.DB, error) {
if driver == "sqlite" {
dir := filepath.Dir(connection)
err := os.MkdirAll(dir, 0755)
if err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
}
db, err := sqlx.Connect(driver, connection)
if err != nil {
return nil, fmt.Errorf("failed to connect: %w", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
slog.Info("database connected", "driver", driver)
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
func Close(db *sqlx.DB) error {
if db != nil {
return db.Close()
}
return nil
}

6
internal/db/embed.go Normal file
View file

@ -0,0 +1,6 @@
package db
import "embed"
//go:embed migrations/*.sql
var migrationsFS embed.FS

55
internal/db/migrate.go Normal file
View file

@ -0,0 +1,55 @@
package db
import (
"database/sql"
"fmt"
"io/fs"
"log/slog"
"github.com/pressly/goose/v3"
)
var dialectMap = map[string]string{
"sqlite": "sqlite3",
"pgx": "postgres",
}
func getDialect(driver string) string {
dialect, ok := dialectMap[driver]
if ok {
return dialect
}
return driver
}
func setupGoose(driver string) error {
err := goose.SetDialect(getDialect(driver))
if err != nil {
return fmt.Errorf("failed to set dialect: %w", err)
}
migrationsDir, err := fs.Sub(migrationsFS, "migrations")
if err != nil {
return fmt.Errorf("failed to create sub-filesystem migrations directory: %w", err)
}
goose.SetBaseFS(migrationsDir)
return nil
}
func RunMigrations(db *sql.DB, driver string) error {
err := setupGoose(driver)
if err != nil {
return err
}
err = goose.Up(db, ".")
if err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
slog.Info("migrations completed successfully")
return nil
}

View file

@ -0,0 +1,21 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NULL, -- Allow null for passwordless login
pending_email TEXT NULL, -- Store new email when changing email
email_verified_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_passwordless ON users(id) WHERE password_hash IS NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS idx_users_passwordless;
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;
-- +goose StatementEnd

19
internal/routes/routes.go Normal file
View file

@ -0,0 +1,19 @@
package routes
import (
"io/fs"
"net/http"
"git.juancwu.dev/juancwu/budgething/assets"
"git.juancwu.dev/juancwu/budgething/internal/app"
)
func SetupRoutes(a *app.App) http.Handler {
mux := http.NewServeMux()
// Static
sub, _ := fs.Sub(assets.AssetsFS, ".")
mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))
return mux
}

View file

@ -0,0 +1,126 @@
// templui component accordion - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/accordion
package accordion
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Accordion(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<details
if p.ID != "" {
id={ p.ID }
}
name="accordion"
class={
utils.TwMerge(
"group border-b last:border-b-0",
"[&[open]>summary>svg]:rotate-180",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</details>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<summary
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex flex-1 items-start justify-between gap-4 py-4",
"text-left text-sm font-medium",
"transition-all hover:underline cursor-pointer",
"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring rounded-md",
"disabled:pointer-events-none disabled:opacity-50",
"list-none [&::-webkit-details-marker]:hidden",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none",
})
</summary>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"pt-0 pb-4 text-sm overflow-hidden",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}

View file

@ -0,0 +1,110 @@
// templui component alert - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/alert
package alert
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Variant string
const (
VariantDefault Variant = "default"
VariantDestructive Variant = "destructive"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Variant Variant
}
type TitleProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Alert(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
data-slot="alert"
class={
utils.TwMerge(
"relative w-full rounded-lg border px-4 py-3 text-sm",
"grid has-[>svg]:grid-cols-[1rem_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start",
"[&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variantClasses(p.Variant),
p.Class,
),
}
role="alert"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Title(props ...TitleProps) {
{{ var p TitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<h5
if p.ID != "" {
id={ p.ID }
}
data-slot="alert-title"
class={
utils.TwMerge(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</h5>
}
templ Description(props ...DescriptionProps) {
{{ var p DescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
data-slot="alert-description"
class={
utils.TwMerge(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
func variantClasses(variant Variant) string {
switch variant {
case VariantDestructive:
return "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90"
default:
return "bg-card text-card-foreground"
}
}

View file

@ -0,0 +1,63 @@
// templui component aspectratio - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/aspect-ratio
package aspectratio
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Ratio string
const (
RatioAuto Ratio = "auto"
RatioSquare Ratio = "square"
RatioVideo Ratio = "video"
RatioPortrait Ratio = "portrait"
RatioWide Ratio = "wide"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Ratio Ratio
}
templ AspectRatio(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"relative w-full",
ratioClass(p.Ratio),
p.Class,
),
}
{ p.Attributes... }
>
<div class="absolute inset-0">
{ children... }
</div>
</div>
}
func ratioClass(ratio Ratio) string {
switch ratio {
case RatioSquare:
return "aspect-square"
case RatioVideo:
return "aspect-video"
case RatioPortrait:
return "aspect-[3/4]"
case RatioWide:
return "aspect-[2/1]"
case RatioAuto:
return "aspect-auto"
default:
return "aspect-auto"
}
}

View file

@ -0,0 +1,97 @@
// templui component avatar - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/avatar
package avatar
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type ImageProps struct {
ID string
Class string
Attributes templ.Attributes
Alt string
Src string
}
type FallbackProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Avatar(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
data-tui-avatar
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Image(props ...ImageProps) {
{{ var p ImageProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<img
data-tui-avatar-image
if p.ID != "" {
id={ p.ID }
}
if p.Src != "" {
src={ p.Src }
}
alt={ p.Alt }
class={
utils.TwMerge(
"aspect-square h-full w-full",
p.Class,
),
}
{ p.Attributes... }
/>
}
templ Fallback(props ...FallbackProps) {
{{ var p FallbackProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
data-tui-avatar-fallback
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</span>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/avatar.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,59 @@
// templui component badge - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/badge
package badge
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Variant string
const (
VariantDefault Variant = "default"
VariantSecondary Variant = "secondary"
VariantDestructive Variant = "destructive"
VariantOutline Variant = "outline"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Variant Variant
}
templ Badge(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"transition-[color,box-shadow] overflow-hidden",
p.variantClasses(),
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</span>
}
func (p Props) variantClasses() string {
switch p.Variant {
case VariantDestructive:
return "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60"
case VariantOutline:
return "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"
case VariantSecondary:
return "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90"
default:
return "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90"
}
}

View file

@ -0,0 +1,176 @@
// templui component breadcrumb - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/breadcrumb
package breadcrumb
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type ListProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
Current bool
}
type LinkProps struct {
ID string
Class string
Attributes templ.Attributes
Href string
}
type SeparatorProps struct {
ID string
Class string
Attributes templ.Attributes
UseCustom bool
}
templ Breadcrumb(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<nav
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex",
p.Class,
),
}
aria-label="Breadcrumb"
{ p.Attributes... }
>
{ children... }
</nav>
}
templ List(props ...ListProps) {
{{ var p ListProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<ol
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex items-center flex-wrap gap-1 text-sm",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</ol>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<li
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex items-center",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</li>
}
templ Link(props ...LinkProps) {
{{ var p LinkProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<a
if p.ID != "" {
id={ p.ID }
}
if p.Href != "" {
href={ templ.SafeURL(p.Href) }
}
class={
utils.TwMerge(
"text-muted-foreground hover:text-foreground hover:underline flex items-center gap-1.5 transition-colors",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</a>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"mx-2 text-muted-foreground",
p.Class,
),
}
{ p.Attributes... }
>
if p.UseCustom {
{ children... }
} else {
@icon.ChevronRight(icon.Props{Size: 14, Class: "text-muted-foreground"})
}
</span>
}
templ Page(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"font-medium text-foreground flex items-center gap-1.5",
p.Class,
),
}
aria-current="page"
{ p.Attributes... }
>
{ children... }
</span>
}

View file

@ -0,0 +1,152 @@
// templui component button - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/button
package button
import (
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strings"
)
type Variant string
type Size string
type Type string
const (
VariantDefault Variant = "default"
VariantDestructive Variant = "destructive"
VariantOutline Variant = "outline"
VariantSecondary Variant = "secondary"
VariantGhost Variant = "ghost"
VariantLink Variant = "link"
)
const (
TypeButton Type = "button"
TypeReset Type = "reset"
TypeSubmit Type = "submit"
)
const (
SizeDefault Size = "default"
SizeSm Size = "sm"
SizeLg Size = "lg"
SizeIcon Size = "icon"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Variant Variant
Size Size
FullWidth bool
Href string
Target string
Disabled bool
Type Type
Form string
}
templ Button(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Type == "" {
{{ p.Type = TypeButton }}
}
if p.Href != "" && !p.Disabled {
<a
if p.ID != "" {
id={ p.ID }
}
href={ templ.SafeURL(p.Href) }
if p.Target != "" {
target={ p.Target }
}
class={
utils.TwMerge(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all",
"disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0",
"outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"cursor-pointer",
p.variantClasses(),
p.sizeClasses(),
p.modifierClasses(),
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</a>
} else {
<button
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all",
"disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0",
"outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"cursor-pointer",
p.variantClasses(),
p.sizeClasses(),
p.modifierClasses(),
p.Class,
),
}
if p.Type != "" {
type={ string(p.Type) }
}
if p.Form != "" {
form={ p.Form }
}
disabled?={ p.Disabled }
{ p.Attributes... }
>
{ children... }
</button>
}
}
func (b Props) variantClasses() string {
switch b.Variant {
case VariantDestructive:
return "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60"
case VariantOutline:
return "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
case VariantSecondary:
return "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80"
case VariantGhost:
return "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50"
case VariantLink:
return "text-primary underline-offset-4 hover:underline"
default:
return "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
}
}
func (b Props) sizeClasses() string {
switch b.Size {
case SizeSm:
return "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5"
case SizeLg:
return "h-10 rounded-md px-6 has-[>svg]:px-4"
case SizeIcon:
return "size-9"
default: // SizeDefault
return "h-9 px-4 py-2 has-[>svg]:px-3"
}
}
func (b Props) modifierClasses() string {
classes := []string{}
if b.FullWidth {
classes = append(classes, "w-full")
}
return strings.Join(classes, " ")
}

View file

@ -0,0 +1,195 @@
// templui component calendar - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/calendar
package calendar
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
"time"
)
type LocaleTag string
var (
LocaleDefaultTag = LocaleTag("en-US")
LocaleTagChinese = LocaleTag("zh-CN")
LocaleTagFrench = LocaleTag("fr-FR")
LocaleTagGerman = LocaleTag("de-DE")
LocaleTagItalian = LocaleTag("it-IT")
LocaleTagJapanese = LocaleTag("ja-JP")
LocaleTagPortuguese = LocaleTag("pt-PT")
LocaleTagSpanish = LocaleTag("es-ES")
)
type Day int
var (
Sunday = Day(0)
Monday = Day(1)
Tuesday = Day(2)
Wednesday = Day(3)
Thursday = Day(4)
Friday = Day(5)
Saturday = Day(6)
)
type Props struct {
ID string
Class string
LocaleTag LocaleTag
Value *time.Time
Name string
InitialMonth int // Optional: 0-11 (Default: current or from Value). Controls the initially displayed month view.
InitialYear int // Optional: (Default: current or from Value). Controls the initially displayed year view.
StartOfWeek *Day // Optional: 0-6 [Sun-Sat] (Default: 1).
RenderHiddenInput bool // Optional: Whether to render the hidden input (Default: true). Set to false when used inside DatePicker.
}
templ Calendar(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
if p.ID == "" {
p.ID = utils.RandomID() + "-calendar"
}
if p.Name == "" {
// Should be provided by parent (e.g., DatePicker or in standalone usage)
p.Name = p.ID + "-value" // Fallback name
}
if p.LocaleTag == "" {
p.LocaleTag = LocaleDefaultTag
}
// Default to rendering hidden input unless explicitly set to false
if p.RenderHiddenInput == false && len(props) > 0 {
// Only respect false if it was explicitly passed
p.RenderHiddenInput = props[0].RenderHiddenInput
} else {
p.RenderHiddenInput = true
}
initialStartOfWeek := Monday
if p.StartOfWeek != nil {
initialStartOfWeek = *p.StartOfWeek
}
initialView := time.Now()
if p.Value != nil {
initialView = *p.Value
}
initialMonth := p.InitialMonth
initialYear := p.InitialYear
// Use year from initialView if InitialYear prop is invalid/unset (<= 0)
if initialYear <= 0 {
initialYear = initialView.Year()
}
// Use month from initialView if InitialMonth prop is invalid OR
// if InitialMonth is default 0 AND InitialYear was also defaulted (meaning neither was likely set explicitly)
if (initialMonth < 0 || initialMonth > 11) || (initialMonth == 0 && p.InitialYear <= 0) {
initialMonth = int(initialView.Month()) - 1 // time.Month is 1-12
}
initialSelectedISO := ""
if p.Value != nil {
initialSelectedISO = p.Value.Format("2006-01-02")
}
// For SelectBox display
currentMonth := initialMonth
currentYear := initialYear
// Generate short month names (English only, JS will update with localized)
monthNames := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
}}
<div class={ p.Class } id={ p.ID + "-wrapper" } data-tui-calendar-wrapper="true">
if p.RenderHiddenInput {
<input
type="hidden"
name={ p.Name }
value={ initialSelectedISO }
id={ p.ID + "-hidden" }
data-tui-calendar-hidden-input
/>
}
<div
id={ p.ID }
data-tui-calendar-container="true"
data-tui-calendar-locale-tag={ string(p.LocaleTag) }
data-tui-calendar-initial-month={ strconv.Itoa(initialMonth) }
data-tui-calendar-initial-year={ strconv.Itoa(initialYear) }
data-tui-calendar-selected-date={ initialSelectedISO }
data-tui-calendar-start-of-week={ int(initialStartOfWeek) }
>
<!-- Calendar Header -->
<div class="flex items-center gap-2 mb-4">
<button
type="button"
data-tui-calendar-prev
class="inline-flex items-center justify-center rounded-md text-sm font-medium h-7 w-7 hover:bg-accent hover:text-accent-foreground focus:outline-none disabled:opacity-50 shrink-0"
>
@icon.ChevronLeft()
</button>
<div class="flex gap-2 flex-1 min-w-0">
<!-- Month Select -->
<div class="relative flex-1 has-[:focus]:border-ring border border-input shadow-xs has-[:focus]:ring-ring/50 has-[:focus]:ring-[3px] rounded-md">
<select
id={ p.ID + "-month-select" }
data-tui-calendar-month-select
class="absolute inset-0 opacity-0 cursor-pointer w-full"
aria-label="Choose the Month"
>
for i := 0; i < 12; i++ {
<option value={ strconv.Itoa(i) } selected?={ i == currentMonth } data-tui-calendar-month-index={ strconv.Itoa(i) }>
{ monthNames[i] }
</option>
}
</select>
<span class="select-none font-medium rounded-md px-2 flex items-center justify-center gap-1 text-sm h-7 pointer-events-none" aria-hidden="true">
<span id={ p.ID + "-month-value" }>{ monthNames[currentMonth] }</span>
@icon.ChevronDown(icon.Props{Size: 14, Class: "text-muted-foreground"})
</span>
</div>
<!-- Year Select -->
<div class="relative flex-1 has-[:focus]:border-ring border border-input shadow-xs has-[:focus]:ring-ring/50 has-[:focus]:ring-[3px] rounded-md">
<select
id={ p.ID + "-year-select" }
data-tui-calendar-year-select
class="absolute inset-0 opacity-0 cursor-pointer w-full"
aria-label="Choose the Year"
>
for year := 2100; year >= 1900; year-- {
<option value={ strconv.Itoa(year) } selected?={ year == currentYear }>
{ strconv.Itoa(year) }
</option>
}
</select>
<span class="select-none font-medium rounded-md px-2 flex items-center justify-center gap-1 text-sm h-7 pointer-events-none" aria-hidden="true">
<span id={ p.ID + "-year-value" }>{ strconv.Itoa(currentYear) }</span>
@icon.ChevronDown(icon.Props{Size: 14, Class: "text-muted-foreground"})
</span>
</div>
</div>
<button
type="button"
data-tui-calendar-next
class="inline-flex items-center justify-center rounded-md text-sm font-medium h-7 w-7 hover:bg-accent hover:text-accent-foreground focus:outline-none disabled:opacity-50 shrink-0"
>
@icon.ChevronRight()
</button>
</div>
<!-- Weekday Headers -->
<div data-tui-calendar-weekdays class="grid grid-cols-7 gap-1 mb-1 place-items-center"></div>
<!-- Calendar Day Grid -->
<div data-tui-calendar-days class="grid grid-cols-7 gap-1 place-items-center"></div>
</div>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/calendar.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,167 @@
// templui component card - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/card
package card
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type HeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TitleProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type FooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Card(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"w-full rounded-lg border bg-card text-card-foreground shadow-xs",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Header(props ...HeaderProps) {
{{ var p HeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex flex-col space-y-1.5 p-6 pb-0",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Title(props ...TitleProps) {
{{ var p TitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<h3
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"text-lg font-semibold leading-none tracking-tight",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</h3>
}
templ Description(props ...DescriptionProps) {
{{ var p DescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<p
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"text-sm text-muted-foreground",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</p>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"p-6",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Footer(props ...FooterProps) {
{{ var p FooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex items-center p-6 pt-0",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}

View file

@ -0,0 +1,211 @@
// templui component carousel - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/carousel
package carousel
import (
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Autoplay bool
Interval int
Loop bool
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type PreviousProps struct {
ID string
Class string
Attributes templ.Attributes
}
type NextProps struct {
ID string
Class string
Attributes templ.Attributes
}
type IndicatorsProps struct {
ID string
Class string
Attributes templ.Attributes
Count int
}
templ Carousel(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"relative overflow-hidden w-full",
p.Class,
),
}
data-tui-carousel
data-tui-carousel-current="0"
data-tui-carousel-autoplay={ strconv.FormatBool(p.Autoplay) }
data-tui-carousel-interval={ fmt.Sprintf("%d", func() int {
if p.Interval == 0 {
return 5000
}
return p.Interval
}()) }
data-tui-carousel-loop={ strconv.FormatBool(p.Loop) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex h-full w-full transition-transform duration-500 ease-in-out cursor-grab",
p.Class,
),
}
data-tui-carousel-track
{ p.Attributes... }
>
{ children... }
</div>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex-shrink-0 w-full h-full relative",
p.Class,
),
}
data-tui-carousel-item
{ p.Attributes... }
>
{ children... }
</div>
}
templ Previous(props ...PreviousProps) {
{{ var p PreviousProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<button
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"absolute left-2 top-1/2 transform -translate-y-1/2 p-2 rounded-full bg-black/20 text-white hover:bg-black/40 focus:outline-none",
p.Class,
),
}
data-tui-carousel-prev
aria-label="Previous slide"
type="button"
{ p.Attributes... }
>
@icon.ChevronLeft()
</button>
}
templ Next(props ...NextProps) {
{{ var p NextProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<button
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-full bg-black/20 text-white hover:bg-black/40 focus:outline-none",
p.Class,
),
}
data-tui-carousel-next
aria-label="Next slide"
type="button"
{ p.Attributes... }
>
@icon.ChevronRight()
</button>
}
templ Indicators(props ...IndicatorsProps) {
{{ var p IndicatorsProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2",
p.Class,
),
}
{ p.Attributes... }
>
for i := 0; i < p.Count; i++ {
<button
class={
utils.TwMerge(
"w-3 h-3 rounded-full bg-foreground/30 hover:bg-foreground/50 focus:outline-none transition-colors",
utils.If(i == 0, "bg-primary"),
),
}
data-tui-carousel-indicator={ strconv.Itoa(i) }
data-tui-carousel-active={ utils.IfElse(i == 0, "true", "false") }
aria-label={ fmt.Sprintf("Go to slide %d", i+1) }
type="button"
></button>
}
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/carousel.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,113 @@
// templui component chart - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/charts
package chart
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Variant string
const (
VariantBar Variant = "bar"
VariantLine Variant = "line"
VariantPie Variant = "pie"
VariantDoughnut Variant = "doughnut"
VariantRadar Variant = "radar"
)
type Dataset struct {
Label string `json:"label"`
Data []float64 `json:"data"`
BorderWidth int `json:"borderWidth,omitempty"`
BorderColor interface{} `json:"borderColor,omitempty"`
BackgroundColor interface{} `json:"backgroundColor,omitempty"`
Tension float64 `json:"tension,omitempty"`
Fill bool `json:"fill,omitempty"`
Stepped bool `json:"stepped,omitempty"`
}
type Options struct {
Responsive bool `json:"responsive,omitempty"`
Legend bool `json:"legend,omitempty"`
}
type Data struct {
Labels []string `json:"labels"`
Datasets []Dataset `json:"datasets"`
}
type Config struct {
Type Variant `json:"type"`
Data Data `json:"data"`
Options Options `json:"options,omitempty"`
ShowLegend bool `json:"showLegend,omitempty"`
ShowXAxis bool `json:"showXAxis"`
ShowYAxis bool `json:"showYAxis"`
ShowXLabels bool `json:"showXLabels"`
ShowYLabels bool `json:"showYLabels"`
ShowXGrid bool `json:"showXGrid"`
ShowYGrid bool `json:"showYGrid"`
Horizontal bool `json:"horizontal"`
Stacked bool `json:"stacked"`
}
type Props struct {
ID string
Variant Variant
Data Data
Options Options
ShowLegend bool
ShowXAxis bool
ShowYAxis bool
ShowXLabels bool
ShowYLabels bool
ShowXGrid bool
ShowYGrid bool
Horizontal bool
Stacked bool
Class string
Attributes templ.Attributes
}
templ Chart(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = "chart-" + utils.RandomID() }}
}
{{ canvasId := p.ID + "-canvas" }}
{{ dataId := p.ID + "-data" }}
<div
id={ p.ID }
class={
utils.TwMerge(
"chart-container relative",
p.Class),
}
{ p.Attributes... }
>
<canvas id={ canvasId } data-tui-chart-id={ dataId }></canvas>
</div>
{{
chartConfig := Config{
Type: p.Variant,
Data: p.Data,
Options: p.Options,
ShowLegend: p.ShowLegend,
ShowXAxis: p.ShowXAxis,
ShowYAxis: p.ShowYAxis,
ShowXLabels: p.ShowXLabels,
ShowYLabels: p.ShowYLabels,
ShowXGrid: p.ShowXGrid,
ShowYGrid: p.ShowYGrid,
Horizontal: p.Horizontal,
Stacked: p.Stacked,
}
}}
@templ.JSONScript(dataId, chartConfig)
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/chart.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,69 @@
// templui component checkbox - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/checkbox
package checkbox
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Value string
Disabled bool
Checked bool
Form string
Icon templ.Component
}
templ Checkbox(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div class="relative inline-flex items-center">
<input
checked?={ p.Checked }
disabled?={ p.Disabled }
if p.ID != "" {
id={ p.ID }
}
if p.Name != "" {
name={ p.Name }
}
if p.Value != "" {
value={ p.Value }
} else {
value="on"
}
if p.Form != "" {
form={ p.Form }
}
type="checkbox"
class={
utils.TwMerge(
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs",
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring",
"disabled:cursor-not-allowed disabled:opacity-50",
"checked:bg-primary checked:text-primary-foreground checked:border-primary",
"appearance-none cursor-pointer transition-shadow",
"relative",
p.Class,
),
}
{ p.Attributes... }
/>
<div
class="absolute left-0 top-0 h-4 w-4 pointer-events-none flex items-center justify-center text-primary-foreground opacity-0 peer-checked:opacity-100"
>
if p.Icon != nil {
@p.Icon
} else {
@icon.Check(icon.Props{Size: 14})
}
</div>
</div>
}

View file

@ -0,0 +1,56 @@
// templui component code - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/code
package code
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attrs templ.Attributes
Language string
CodeClass string
}
templ Code(props ...Props) {
// Highlight.js with theme switching
<link
id="highlight-theme"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css"
rel="stylesheet"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = "code-" + utils.RandomID() }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative code-component", p.Class) }
data-tui-code-component
{ p.Attrs... }
>
<pre class="overflow-hidden!">
<code
data-tui-code-block
class={
utils.TwMerge(
"language-"+p.Language,
"overflow-y-auto! rounded-md block text-sm max-h-[500px]",
"hljs-target",
p.CodeClass,
),
}
>
{ children... }
</code>
</pre>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/code.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,86 @@
// templui component collapsible - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/collapsible
package collapsible
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
Open bool
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Collapsible(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<div
id={ p.ID }
class={ utils.TwMerge("", p.Class) }
data-tui-collapsible="root"
data-tui-collapsible-state={ utils.IfElse(p.Open, "open", "closed") }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("", p.Class) }
data-tui-collapsible="trigger"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"grid grid-rows-[0fr] transition-[grid-template-rows] duration-200 ease-out [[data-tui-collapsible-state=open]_&]:grid-rows-[1fr]",
p.Class,
) }
data-tui-collapsible="content"
{ p.Attributes... }
>
<div class="overflow-hidden">
{ children... }
</div>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/collapsible.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,48 @@
// templui component copybutton - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/copy-button
package copybutton
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string // Optional button ID
Class string // Custom CSS classes
Attrs templ.Attributes // Additional HTML attributes
TargetID string // Required - ID of element to copy from
}
templ CopyButton(props Props) {
{{ var p = props }}
if p.ID == "" {
{{ p.ID = "copybutton-" + utils.RandomID() }}
}
<div
data-copy-button
data-target-id={ p.TargetID }
class="inline-block"
>
@button.Button(button.Props{
ID: p.ID,
Class: utils.TwMerge("h-7 w-7 text-muted-foreground hover:text-accent-foreground", p.Class),
Attributes: p.Attrs,
Size: button.SizeIcon,
Variant: button.VariantGhost,
Type: button.TypeButton,
}) {
<span data-copy-icon-clipboard>
@icon.Clipboard(icon.Props{Size: 16})
</span>
<span data-copy-icon-check class="hidden">
@icon.Check(icon.Props{Size: 16})
</span>
}
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/copybutton.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,158 @@
// templui component datepicker - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/date-picker
package datepicker
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/calendar"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/card"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"time"
)
type Format string
type LocaleTag string
const (
FormatLOCALE_SHORT Format = "locale-short" // Locale-specific short format (e.g., MM/DD/YY or DD.MM.YY)
FormatLOCALE_MEDIUM Format = "locale-medium" // Locale-specific medium format (e.g., Jan 5, 2024 or 5. Jan. 2024)
FormatLOCALE_LONG Format = "locale-long" // Locale-specific long format (e.g., January 5, 2024 or 5. Januar 2024)
FormatLOCALE_FULL Format = "locale-full" // Locale-specific full format (e.g., Monday, January 5, 2024 or Montag, 5. Januar 2024)
)
// Common Locale (BCP 47)
var (
LocaleDefaultTag = LocaleTag("en-US")
LocaleTagChinese = LocaleTag("zh-CN")
LocaleTagFrench = LocaleTag("fr-FR")
LocaleTagGerman = LocaleTag("de-DE")
LocaleTagItalian = LocaleTag("it-IT")
LocaleTagJapanese = LocaleTag("ja-JP")
LocaleTagPortuguese = LocaleTag("pt-PT")
LocaleTagSpanish = LocaleTag("es-ES")
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Value time.Time
Form string
Format Format // Controls the display format using Intl dateStyle options.
LocaleTag LocaleTag // BCP 47 Locale Tag (e.g., "en-US", "es-ES"). Determines language and regional format defaults.
StartOfWeek *calendar.Day // Optional: 0-6 [Sun-Sat] (Default: 1).
Placeholder string
Disabled bool
HasError bool
}
templ DatePicker(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
if p.ID == "" {
p.ID = utils.RandomID()
}
if p.Name == "" {
p.Name = p.ID
}
if p.Placeholder == "" {
p.Placeholder = "Select a date"
}
if p.LocaleTag == "" {
p.LocaleTag = LocaleDefaultTag
}
if p.Format == "" {
p.Format = FormatLOCALE_MEDIUM
}
var contentID = p.ID + "-content"
var valuePtr *time.Time
var initialSelectedISO string
if !p.Value.IsZero() {
valuePtr = &p.Value
initialSelectedISO = p.Value.Format("2006-01-02")
}
}}
<div class="relative inline-block w-full">
<input
type="hidden"
name={ p.Name }
value={ initialSelectedISO }
if p.Form != "" {
form={ p.Form }
}
id={ p.ID + "-hidden" }
data-tui-datepicker-hidden-input
/>
@popover.Trigger(popover.TriggerProps{For: contentID}) {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-datepicker": "true",
"data-tui-datepicker-display-format": string(p.Format),
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
"data-tui-datepicker-placeholder": p.Placeholder,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
if p.Placeholder != "" {
<span data-tui-datepicker-display class={ "text-left grow text-muted-foreground" }>
{ p.Placeholder }
</span>
}
<span class="text-muted-foreground flex items-center ml-2">
@icon.Calendar(icon.Props{Size: 16})
</span>
}
}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Class: "p-0",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@card.Content(card.ContentProps{
Class: "p-3",
}) {
@calendar.Calendar(calendar.Props{
ID: p.ID + "-calendar-instance", // Pass ID for calendar instance
LocaleTag: calendar.LocaleTag(p.LocaleTag), // Pass locale tag to calendar
StartOfWeek: p.StartOfWeek, // Pass start of week to calendar
Value: valuePtr, // Pass pointer to value
RenderHiddenInput: false, // Don't render hidden input inside popover
})
}
}
}
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/datepicker.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,332 @@
// templui component dialog - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/dialog
package dialog
import (
"context"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type contextKey string
const (
instanceKey contextKey = "dialogInstance"
openKey contextKey = "dialogOpen"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
DisableClickAway bool
DisableESC bool
Open bool
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Reference to a specific dialog ID (for external triggers)
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
HideCloseButton bool
Open bool // Initial open state for standalone usage (when no context)
DisableAutoFocus bool
}
type CloseProps struct {
ID string
Class string
Attributes templ.Attributes
For string // ID of the dialog to close (optional, defaults to closest dialog)
}
type HeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type FooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TitleProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Dialog(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ instanceID := p.ID }}
if instanceID == "" {
{{ instanceID = utils.RandomID() }}
}
{{ ctx = context.WithValue(ctx, instanceKey, instanceID) }}
{{ ctx = context.WithValue(ctx, openKey, p.Open) }}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog
data-dialog-instance={ instanceID }
if p.DisableClickAway {
data-tui-dialog-disable-click-away="true"
}
if p.DisableESC {
data-tui-dialog-disable-esc="true"
}
class={ utils.TwMerge("", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ instanceID := "" }}
// Explicit For prop takes priority over inherited context
if p.For != "" {
{{ instanceID = p.For }}
} else if val := ctx.Value(instanceKey); val != nil {
{{ instanceID = val.(string) }}
}
<span
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog-trigger={ instanceID }
data-dialog-instance={ instanceID }
data-tui-dialog-trigger-open="false"
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Start with prop values as defaults
{{ instanceID := p.ID }}
{{ open := p.Open }}
// Override with context values if available
if val := ctx.Value(instanceKey); val != nil {
{{ instanceID = val.(string) }}
}
if val := ctx.Value(openKey); val != nil {
{{ open = val.(bool) }}
}
// Apply defaults if still empty
if instanceID == "" {
{{ instanceID = utils.RandomID() }}
}
<!-- Overlay -->
<div
class={ utils.TwMerge(
"fixed inset-0 z-50 bg-black/50",
"transition-opacity duration-300",
"data-[tui-dialog-open=false]:opacity-0",
"data-[tui-dialog-open=true]:opacity-100",
"data-[tui-dialog-open=false]:pointer-events-none",
"data-[tui-dialog-open=true]:pointer-events-auto",
"data-[tui-dialog-hidden=true]:!hidden",
) }
data-tui-dialog-backdrop
data-dialog-instance={ instanceID }
if open {
data-tui-dialog-open="true"
} else {
data-tui-dialog-open="false"
data-tui-dialog-hidden="true"
}
></div>
<!-- Content -->
<div
class={
utils.TwMerge(
// Base positioning
"fixed z-50 left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]",
// Style
"bg-background rounded-lg border shadow-lg",
// Layout
"grid gap-4 p-6",
// Size
"w-full max-w-[calc(100%-2rem)] sm:max-w-lg",
// Transitions
"transition-all duration-200",
// Scale animation
"data-[tui-dialog-open=false]:scale-95",
"data-[tui-dialog-open=true]:scale-100",
// Opacity
"data-[tui-dialog-open=false]:opacity-0",
"data-[tui-dialog-open=true]:opacity-100",
// Pointer events
"data-[tui-dialog-open=false]:pointer-events-none",
"data-[tui-dialog-open=true]:pointer-events-auto",
// Hidden state
"data-[tui-dialog-hidden=true]:!hidden",
p.Class,
),
}
data-tui-dialog-content
data-dialog-instance={ instanceID }
if p.DisableAutoFocus {
data-tui-dialog-disable-autofocus="true"
}
if open {
data-tui-dialog-open="true"
} else {
data-tui-dialog-open="false"
data-tui-dialog-hidden="true"
}
{ p.Attributes... }
>
{ children... }
if !p.HideCloseButton {
<button
class={ utils.TwMerge(
// Positioning
"absolute top-4 right-4",
// Style
"rounded-xs opacity-70",
// Interactions
"transition-opacity hover:opacity-100",
// Focus states
"focus:outline-none focus:ring-2",
"focus:ring-ring focus:ring-offset-2",
"ring-offset-background",
// Hover/Data states
"data-[tui-dialog-open=true]:bg-accent",
"data-[tui-dialog-open=true]:text-muted-foreground",
// Disabled state
"disabled:pointer-events-none",
// Icon styles
"[&_svg]:pointer-events-none",
"[&_svg]:shrink-0",
"[&_svg:not([class*='size-'])]:size-4",
) }
data-tui-dialog-close={ instanceID }
aria-label="Close"
type="button"
>
@icon.X()
<span class="sr-only">Close</span>
</button>
}
</div>
}
templ Close(props ...CloseProps) {
{{ var p CloseProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
if p.For != "" {
data-tui-dialog-close={ p.For }
} else {
data-tui-dialog-close
}
class={ utils.TwMerge("contents cursor-pointer", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Header(props ...HeaderProps) {
{{ var p HeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col gap-2 text-center sm:text-left", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Footer(props ...FooterProps) {
{{ var p FooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Title(props ...TitleProps) {
{{ var p TitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<h2
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-lg leading-none font-semibold", p.Class) }
{ p.Attributes... }
>
{ children... }
</h2>
}
templ Description(props ...DescriptionProps) {
{{ var p DescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<p
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-muted-foreground text-sm", p.Class) }
{ p.Attributes... }
>
{ children... }
</p>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/dialog.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,391 @@
// templui component dropdown - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/dropdown
package dropdown
import (
"context"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Placement = popover.Placement
const (
PlacementTop = popover.PlacementTop
PlacementTopStart = popover.PlacementTopStart
PlacementTopEnd = popover.PlacementTopEnd
PlacementRight = popover.PlacementRight
PlacementRightStart = popover.PlacementRightStart
PlacementRightEnd = popover.PlacementRightEnd
PlacementBottom = popover.PlacementBottom
PlacementBottomStart = popover.PlacementBottomStart
PlacementBottomEnd = popover.PlacementBottomEnd
PlacementLeft = popover.PlacementLeft
PlacementLeftStart = popover.PlacementLeftStart
PlacementLeftEnd = popover.PlacementLeftEnd
)
type contextKey string
var (
contentIDKey contextKey = "contentID"
subContentIDKey contextKey = "subContentID"
)
type Props struct {
ID string
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
Placement Placement
}
type GroupProps struct {
ID string
Class string
Attributes templ.Attributes
}
type LabelProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
Disabled bool
Href string
Target string
PreventClose bool
}
type SeparatorProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ShortcutProps struct {
ID string
Class string
Attributes templ.Attributes
}
type SubProps struct {
ID string
Class string
Attributes templ.Attributes
}
type SubTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type SubContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type PortalProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Dropdown(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
contentID := p.ID
if contentID == "" {
contentID = utils.RandomID()
}
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
{ children... }
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-content-id"
}
}}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
{ children... }
}
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ contentID, ok := ctx.Value(contentIDKey).(string) }}
if !ok {
{{ contentID = "fallback-content-id" }} // Must match fallback in Trigger
}
{{
placement := p.Placement
if placement == "" {
placement = PlacementBottomStart
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: placement,
Class: utils.TwMerge(
"z-50 rounded-md bg-popover p-1 shadow-md focus:outline-none overflow-auto",
"border border-border",
"min-w-[8rem] max-h-[300px]",
p.Class,
),
Attributes: p.Attributes,
Exclusive: true,
}) {
{ children... }
}
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("py-1", p.Class) }
role="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Label(props ...LabelProps) {
{{ var p LabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-2 py-1.5 text-sm font-semibold", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Href != "" {
<a
id={ p.ID }
if p.Href != "" {
href={ templ.SafeURL(p.Href) }
}
if p.Target != "" {
target={ p.Target }
}
class={
utils.TwMerge(
"flex text-left items-center justify-between px-2 py-1.5 text-sm rounded-sm",
utils.If(!p.Disabled, "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-default"),
utils.If(p.Disabled, "opacity-50 pointer-events-none"),
p.Class,
),
}
role="menuitem"
data-tui-dropdown-item
if p.PreventClose {
data-tui-dropdown-prevent-close="true"
}
{ p.Attributes... }
>
{ children... }
</a>
} else {
<button
id={ p.ID }
class={
utils.TwMerge(
"w-full text-left flex items-center justify-between px-2 py-1.5 text-sm rounded-sm",
utils.If(!p.Disabled, "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-default"),
utils.If(p.Disabled, "opacity-50 pointer-events-none"),
p.Class,
),
}
role="menuitem"
data-tui-dropdown-item
disabled?={ p.Disabled }
if p.PreventClose {
data-tui-dropdown-prevent-close="true"
}
{ p.Attributes... }
>
{ children... }
</button>
}
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("h-px my-1 -mx-1 bg-muted", p.Class) }
role="separator"
{ p.Attributes... }
></div>
}
templ Shortcut(props ...ShortcutProps) {
{{ var p ShortcutProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("ml-auto text-xs tracking-widest opacity-60", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Sub(props ...SubProps) {
{{
var p SubProps
if len(props) > 0 {
p = props[0]
}
subContentID := p.ID
if subContentID == "" {
subContentID = utils.RandomID()
}
ctx = context.WithValue(ctx, subContentIDKey, subContentID)
}}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-dropdown-submenu
class={ utils.TwMerge("relative", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ SubTrigger(props ...SubTriggerProps) {
{{
var p SubTriggerProps
if len(props) > 0 {
p = props[0]
}
subContentID, ok := ctx.Value(subContentIDKey).(string)
if !ok {
subContentID = "fallback-subcontent-id"
}
}}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
For: subContentID,
TriggerType: popover.TriggerTypeHover,
}) {
<button
type="button"
data-tui-dropdown-submenu-trigger
class={
utils.TwMerge(
"w-full text-left flex items-center justify-between px-2 py-1.5 text-sm rounded-sm",
"focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-default",
p.Class,
),
}
{ p.Attributes... }
>
<span>
{ children... }
</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-auto">
<path d="M6.5 3L11.5 8L6.5 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
}
}
templ SubContent(props ...SubContentProps) {
{{
var p SubContentProps
if len(props) > 0 {
p = props[0]
}
subContentID, ok := ctx.Value(subContentIDKey).(string)
if !ok {
subContentID = "fallback-subcontent-id"
}
}}
@popover.Content(popover.ContentProps{
ID: subContentID,
Placement: popover.PlacementRightStart,
Offset: -4, // Adjust as needed
HoverDelay: 100, // ms
HoverOutDelay: 200, // ms
Class: utils.TwMerge(
"z-[9999] min-w-[8rem] rounded-md border bg-popover p-1 shadow-lg",
p.Class,
),
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/dropdown.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,138 @@
// templui component form - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/form
package form
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/label"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type MessageVariant string
const (
MessageVariantError MessageVariant = "error"
MessageVariantInfo MessageVariant = "info"
)
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type LabelProps struct {
ID string
Class string
Attributes templ.Attributes
For string
DisabledClass string
}
type DescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MessageProps struct {
ID string
Class string
Attributes templ.Attributes
Variant MessageVariant
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("space-y-2", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ ItemFlex(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("items-center flex space-x-2", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Label(props ...LabelProps) {
{{ var p LabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@label.Label(label.Props{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
For: p.For,
}) {
{ children... }
}
}
templ Description(props ...DescriptionProps) {
{{ var p DescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<p
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("text-sm text-muted-foreground", p.Class) }
{ p.Attributes... }
>
{ children... }
</p>
}
templ Message(props ...MessageProps) {
{{ var p MessageProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<p
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"text-[0.8rem] font-medium",
messageVariantClass(p.Variant),
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</p>
}
func messageVariantClass(variant MessageVariant) string {
switch variant {
case MessageVariantError:
return "text-red-500"
case MessageVariantInfo:
return "text-blue-500"
default:
return ""
}
}

View file

@ -0,0 +1,118 @@
// templui component icon - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/icon
package icon
import (
"context"
"fmt"
"io"
"sync"
"github.com/a-h/templ"
)
// iconContents caches the fully generated SVG strings for icons that have been used,
// keyed by a composite key of name and props to handle different stylings.
var (
iconContents = make(map[string]string)
iconMutex sync.RWMutex
)
// Props defines the properties that can be set for an icon.
type Props struct {
Size int
Color string
Fill string
Stroke string
StrokeWidth string // Stroke Width of Icon, Usage: "2.5"
Class string
}
// Icon returns a function that generates a templ.Component for the specified icon name.
func Icon(name string) func(...Props) templ.Component {
return func(props ...Props) templ.Component {
var p Props
if len(props) > 0 {
p = props[0]
}
// Create a unique key for the cache based on icon name and all relevant props.
// This ensures different stylings of the same icon are cached separately.
cacheKey := fmt.Sprintf("%s|s:%d|c:%s|f:%s|sk:%s|sw:%s|cl:%s",
name, p.Size, p.Color, p.Fill, p.Stroke, p.StrokeWidth, p.Class)
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
iconMutex.RLock()
svg, cached := iconContents[cacheKey]
iconMutex.RUnlock()
if cached {
_, err = w.Write([]byte(svg))
return err
}
// Not cached, generate it
// The actual generation now happens once and is cached.
generatedSvg, err := generateSVG(name, p) // p (Props) is passed to generateSVG
if err != nil {
// Provide more context in the error message
return fmt.Errorf("failed to generate svg for icon '%s' with props %+v: %w", name, p, err)
}
iconMutex.Lock()
iconContents[cacheKey] = generatedSvg
iconMutex.Unlock()
_, err = w.Write([]byte(generatedSvg))
return err
})
}
}
// generateSVG creates an SVG string for the specified icon with the given properties.
// This function is called when an icon-prop combination is not yet in the cache.
func generateSVG(name string, props Props) (string, error) {
// Get the raw, inner SVG content for the icon name from our internal data map.
content, err := getIconContent(name) // This now reads from internalSvgData
if err != nil {
return "", err // Error from getIconContent already includes icon name
}
size := props.Size
if size <= 0 {
size = 24 // Default size
}
fill := props.Fill
if fill == "" {
fill = "none" // Default fill
}
stroke := props.Stroke
if stroke == "" {
stroke = props.Color // Fallback to Color if Stroke is not set
}
if stroke == "" {
stroke = "currentColor" // Default stroke color
}
strokeWidth := props.StrokeWidth
if strokeWidth == "" {
strokeWidth = "2" // Default stroke width
}
// Construct the final SVG string.
// The data-lucide attribute helps identify these as Lucide icons if needed.
return fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"0 0 24 24\" fill=\"%s\" stroke=\"%s\" stroke-width=\"%s\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"%s\" data-lucide=\"icon\">%s</svg>",
size, size, fill, stroke, strokeWidth, props.Class, content), nil
}
// getIconContent retrieves the raw inner SVG content for a given icon name.
// It reads from the pre-generated internalSvgData map from icon_data.go.
func getIconContent(name string) (string, error) {
content, exists := internalSvgData[name]
if !exists {
return "", fmt.Errorf("icon '%s' not found in internalSvgData map", name)
}
return content, nil
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,130 @@
// templui component input - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/input
package input
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Type string
const (
TypeText Type = "text"
TypePassword Type = "password"
TypeEmail Type = "email"
TypeNumber Type = "number"
TypeTel Type = "tel"
TypeURL Type = "url"
TypeSearch Type = "search"
TypeDate Type = "date"
TypeDateTime Type = "datetime-local"
TypeTime Type = "time"
TypeFile Type = "file"
TypeColor Type = "color"
TypeWeek Type = "week"
TypeMonth Type = "month"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Type Type
Form string
Placeholder string
Value string
Disabled bool
Readonly bool
FileAccept string
HasError bool
NoTogglePassword bool
}
templ Input(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Type == "" {
{{ p.Type = TypeText }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<div class="relative w-full">
<input
id={ p.ID }
type={ string(p.Type) }
if p.Name != "" {
name={ p.Name }
}
if p.Placeholder != "" {
placeholder={ p.Placeholder }
}
if p.Value != "" {
value={ p.Value }
}
if p.Type == TypeFile && p.FileAccept != "" {
accept={ p.FileAccept }
}
if p.Form != "" {
form={ p.Form }
}
disabled?={ p.Disabled }
readonly?={ p.Readonly }
if p.HasError {
aria-invalid="true"
}
class={
utils.TwMerge(
// Base styles
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Placeholder
"placeholder:text-muted-foreground",
// File input styles
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Disabled styles
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
utils.If(p.Type == TypePassword && !p.NoTogglePassword, "pr-8"),
p.Class,
),
}
{ p.Attributes... }
/>
if p.Type == TypePassword && !p.NoTogglePassword {
@button.Button(button.Props{
Size: button.SizeIcon,
Variant: button.VariantGhost,
Class: "absolute right-0 top-1/2 -translate-y-1/2 opacity-50 cursor-pointer",
Attributes: templ.Attributes{"data-tui-input-toggle-password": p.ID},
}) {
<span class="icon-open block">
@icon.Eye(icon.Props{
Size: 18,
})
</span>
<span class="icon-closed hidden">
@icon.EyeOff(icon.Props{
Size: 18,
})
</span>
}
}
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/input.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,181 @@
// templui component inputotp - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/input-otp
package inputotp
import (
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Value string
Name string
Form string
HasError bool
}
type GroupProps struct {
ID string
Class string
Attributes templ.Attributes
}
type SlotProps struct {
ID string
Class string
Attributes templ.Attributes
Index int
Type string
Placeholder string
Disabled bool
HasError bool
}
type SeparatorProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ InputOTP(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID + "-container" }
}
if p.Value != "" {
data-tui-inputotp-value={ p.Value }
}
if p.Form != "" {
form={ p.Form }
}
class={
utils.TwMerge(
"flex flex-row items-center gap-2 w-fit",
p.Class,
),
}
data-tui-inputotp
{ p.Attributes... }
>
<input
type="hidden"
if p.ID != "" {
id={ p.ID }
}
if p.Name != "" {
name={ p.Name }
}
if p.HasError {
aria-invalid="true"
}
data-tui-inputotp-value-target
/>
{ children... }
</div>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex gap-2",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</div>
}
templ Slot(props ...SlotProps) {
{{ var p SlotProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Type == "" {
{{ p.Type = "text" }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class="relative"
{ p.Attributes... }
>
<input
type={ p.Type }
inputmode="numeric"
if p.Placeholder != "" {
placeholder={ p.Placeholder }
}
maxlength="1"
class={
utils.TwMerge(
// Base styles - keeping the specific OTP dimensions
"w-10 h-12 text-center rounded-md border border-input bg-transparent text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Placeholder
"placeholder:text-muted-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Disabled styles
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
}
disabled?={ p.Disabled }
if p.HasError {
aria-invalid="true"
}
data-tui-inputotp-index={ strconv.Itoa(p.Index) }
data-tui-inputotp-slot
{ p.Attributes... }
/>
</div>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex items-center text-muted-foreground text-xl",
p.Class,
),
}
{ p.Attributes... }
>
<span>-</span>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/inputotp.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,43 @@
// templui component label - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/label
package label
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
For string
Error string
}
templ Label(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<label
if p.ID != "" {
id={ p.ID }
}
if p.For != "" {
for={ p.For }
}
class={
utils.TwMerge(
"text-sm font-medium leading-none inline-block",
utils.If(len(p.Error) > 0, "text-destructive"),
p.Class,
),
}
data-tui-label-disabled-style="opacity-50 cursor-not-allowed"
{ p.Attributes... }
>
{ children... }
</label>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/label.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,250 @@
// templui component pagination - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/pagination
package pagination
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type LinkProps struct {
ID string
Class string
Attributes templ.Attributes
Href string
IsActive bool
Disabled bool
}
type PreviousProps struct {
ID string
Class string
Attributes templ.Attributes
Href string
Disabled bool
Label string
}
type NextProps struct {
ID string
Class string
Attributes templ.Attributes
Href string
Disabled bool
Label string
}
templ Pagination(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<nav
if p.ID != "" {
id={ p.ID }
}
role="navigation"
aria-label="pagination"
class={ utils.TwMerge("flex flex-wrap justify-center", p.Class) }
{ p.Attributes... }
>
{ children... }
</nav>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<ul
if p.ID != "" {
id={ p.ID }
}
class="flex flex-row items-center gap-1"
{ p.Attributes... }
>
{ children... }
</ul>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<li
if p.ID != "" {
id={ p.ID }
}
{ p.Attributes... }
>
{ children... }
</li>
}
templ Link(props ...LinkProps) {
{{ var p LinkProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Disabled {
@button.Button(button.Props{
ID: p.ID,
Disabled: true,
Size: button.SizeIcon,
Variant: button.VariantGhost,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
} else {
@button.Button(button.Props{
ID: p.ID,
Href: p.Href,
Size: button.SizeIcon,
Variant: button.Variant(buttonVariant(p.IsActive)),
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
}
}
templ Previous(props ...PreviousProps) {
{{ var p PreviousProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@button.Button(button.Props{
ID: p.ID,
Href: p.Href,
Disabled: p.Disabled,
Variant: button.VariantGhost,
Class: utils.TwMerge("gap-1", p.Class),
Attributes: p.Attributes,
}) {
@icon.ChevronLeft(icon.Props{Size: 16})
if p.Label != "" {
<span>{ p.Label }</span>
}
}
}
templ Next(props ...NextProps) {
{{ var p NextProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@button.Button(button.Props{
ID: p.ID,
Href: p.Href,
Disabled: p.Disabled,
Variant: button.VariantGhost,
Class: utils.TwMerge("gap-1", p.Class),
Attributes: p.Attributes,
}) {
if p.Label != "" {
<span>{ p.Label }</span>
}
@icon.ChevronRight(icon.Props{Size: 16})
}
}
templ Ellipsis() {
@icon.Ellipsis(icon.Props{Size: 16})
}
func CreatePagination(currentPage, totalPages, maxVisible int) struct {
CurrentPage int
TotalPages int
Pages []int
HasPrevious bool
HasNext bool
} {
if currentPage < 1 {
currentPage = 1
}
if totalPages < 1 {
totalPages = 1
}
if currentPage > totalPages {
currentPage = totalPages
}
if maxVisible < 1 {
maxVisible = 5
}
start, end := calculateVisibleRange(currentPage, totalPages, maxVisible)
pages := make([]int, 0, end-start+1)
for i := start; i <= end; i++ {
pages = append(pages, i)
}
return struct {
CurrentPage int
TotalPages int
Pages []int
HasPrevious bool
HasNext bool
}{
CurrentPage: currentPage,
TotalPages: totalPages,
Pages: pages,
HasPrevious: currentPage > 1,
HasNext: currentPage < totalPages,
}
}
func calculateVisibleRange(currentPage, totalPages, maxVisible int) (int, int) {
if totalPages <= maxVisible {
return 1, totalPages
}
half := maxVisible / 2
start := currentPage - half
end := currentPage + half
if start < 1 {
end += (1 - start)
start = 1
}
if end > totalPages {
start -= (end - totalPages)
if start < 1 {
start = 1
}
end = totalPages
}
return start, end
}
func buttonVariant(isActive bool) button.Variant {
if isActive {
return button.VariantOutline
}
return button.VariantGhost
}

View file

@ -0,0 +1,135 @@
// templui component popover - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/popover
package popover
import (
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type Placement string
const (
PlacementTop Placement = "top"
PlacementTopStart Placement = "top-start"
PlacementTopEnd Placement = "top-end"
PlacementRight Placement = "right"
PlacementRightStart Placement = "right-start"
PlacementRightEnd Placement = "right-end"
PlacementBottom Placement = "bottom"
PlacementBottomStart Placement = "bottom-start"
PlacementBottomEnd Placement = "bottom-end"
PlacementLeft Placement = "left"
PlacementLeftStart Placement = "left-start"
PlacementLeftEnd Placement = "left-end"
)
type TriggerType string
const (
TriggerTypeHover TriggerType = "hover"
TriggerTypeClick TriggerType = "click"
)
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string
TriggerType TriggerType
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
Placement Placement
Offset int
DisableClickAway bool
DisableESC bool
ShowArrow bool
HoverDelay int
HoverOutDelay int
MatchWidth bool
Exclusive bool
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.TriggerType == "" {
{{ p.TriggerType = TriggerTypeClick }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("group cursor-pointer", p.Class) }
if p.For != "" {
data-tui-popover-trigger={ p.For }
}
data-tui-popover-open="false"
data-tui-popover-type={ string(p.TriggerType) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Placement == "" {
{{ p.Placement = PlacementBottom }}
}
if p.Offset == 0 {
if p.ShowArrow {
{{ p.Offset = 8 }}
} else {
{{ p.Offset = 4 }}
}
}
<div
id={ p.ID }
data-tui-popover-id={ p.ID }
data-tui-popover-open="false"
data-tui-popover-placement={ string(p.Placement) }
data-tui-popover-offset={ strconv.Itoa(p.Offset) }
data-tui-popover-disable-clickaway={ strconv.FormatBool(p.DisableClickAway) }
data-tui-popover-disable-esc={ strconv.FormatBool(p.DisableESC) }
data-tui-popover-show-arrow={ strconv.FormatBool(p.ShowArrow) }
data-tui-popover-hover-delay={ strconv.Itoa(p.HoverDelay) }
data-tui-popover-hover-out-delay={ strconv.Itoa(p.HoverOutDelay) }
data-tui-popover-exclusive={ strconv.FormatBool(p.Exclusive) }
if p.MatchWidth {
data-tui-popover-match-width="true"
}
class={ utils.TwMerge(
"bg-popover rounded-lg border text-popover-foreground text-sm shadow-lg pointer-events-auto absolute z-[9999] hidden top-0 left-0",
p.Class,
) }
{ p.Attributes... }
>
<div class="w-full overflow-hidden">
{ children... }
</div>
if p.ShowArrow {
<div
data-tui-popover-arrow
class="absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border
data-[tui-popover-placement^=top]:-bottom-[5px] data-[tui-popover-placement^=top]:border-t-transparent data-[tui-popover-placement^=top]:border-l-transparent
data-[tui-popover-placement^=bottom]:-top-[5px] data-[tui-popover-placement^=bottom]:border-b-transparent data-[tui-popover-placement^=bottom]:border-r-transparent
data-[tui-popover-placement^=left]:-right-[5px] data-[tui-popover-placement^=left]:border-l-transparent data-[tui-popover-placement^=left]:border-b-transparent
data-[tui-popover-placement^=right]:-left-[5px] data-[tui-popover-placement^=right]:border-r-transparent data-[tui-popover-placement^=right]:border-t-transparent"
></div>
}
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/popover.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,127 @@
// templui component progress - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/progress
package progress
import (
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Size string
type Variant string
const (
SizeSm Size = "sm"
SizeLg Size = "lg"
)
const (
VariantDefault Variant = "default"
VariantSuccess Variant = "success"
VariantDanger Variant = "danger"
VariantWarning Variant = "warning"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Max int
Value int
Label string
ShowValue bool
Size Size
Variant Variant
BarClass string
}
templ Progress(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<div
id={ p.ID }
class={ utils.TwMerge("w-full", p.Class) }
aria-valuemin="0"
aria-valuemax={ fmt.Sprintf("%d", maxValue(p.Max)) }
aria-valuenow={ fmt.Sprintf("%d", p.Value) }
role="progressbar"
{ p.Attributes... }
>
if p.Label != "" || p.ShowValue {
<div class="flex justify-between items-center mb-1">
if p.Label != "" {
<span class="text-sm font-medium">{ p.Label }</span>
}
if p.ShowValue {
<span class="text-sm font-medium">
{ fmt.Sprintf("%d%%", percentage(p.Value, p)) }
</span>
}
</div>
}
<div class="w-full overflow-hidden rounded-full bg-secondary">
<div
data-tui-progress-indicator
class={
utils.TwMerge(
"h-full rounded-full transition-all",
sizeClasses(p.Size),
variantClasses(p.Variant),
p.BarClass,
),
}
></div>
</div>
</div>
}
func maxValue(max int) int {
if max <= 0 {
return 100
}
return max
}
func percentage(value int, props Props) int {
max := maxValue(props.Max)
if value < 0 {
value = 0
}
if value > max {
value = max
}
return (value * 100) / max
}
func sizeClasses(size Size) string {
switch size {
case SizeSm:
return "h-1"
case SizeLg:
return "h-4"
default:
return "h-2.5"
}
}
func variantClasses(variant Variant) string {
switch variant {
case VariantSuccess:
return "bg-green-500"
case VariantDanger:
return "bg-destructive"
case VariantWarning:
return "bg-yellow-500"
default:
return "bg-primary"
}
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/progress.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,57 @@
// templui component radio - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/radio
package radio
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Value string
Form string
Disabled bool
Checked bool
}
templ Radio(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<input
type="radio"
if p.ID != "" {
id={ p.ID }
}
if p.Name != "" {
name={ p.Name }
}
if p.Value != "" {
value={ p.Value }
}
if p.Form != "" {
form={ p.Form }
}
checked?={ p.Checked }
disabled?={ p.Disabled }
class={
utils.TwMerge(
"relative h-4 w-4",
"before:absolute before:left-1/2 before:top-1/2",
"before:h-1.5 before:w-1.5 before:-translate-x-1/2 before:-translate-y-1/2",
"appearance-none rounded-full",
"border-2 border-primary",
"before:content[''] before:rounded-full before:bg-background",
"checked:border-primary checked:bg-primary",
"checked:before:visible",
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
"focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:cursor-not-allowed",
p.Class,
),
}
{ p.Attributes... }
/>
}

View file

@ -0,0 +1,193 @@
// templui component rating - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/rating
package rating
import (
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type Style string
const (
StyleStar Style = "star"
StyleHeart Style = "heart"
StyleEmoji Style = "emoji"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Value float64
ReadOnly bool
Precision float64
Name string
Form string
OnlyInteger bool
}
type GroupProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
Value int
Style Style
}
templ Rating(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ p.setDefaults() }}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-rating-component
data-tui-rating-initial-value={ fmt.Sprintf("%.2f", p.Value) }
data-tui-rating-precision={ fmt.Sprintf("%.2f", p.Precision) }
data-tui-rating-readonly={ strconv.FormatBool(p.ReadOnly) }
if p.Name != "" {
data-tui-rating-name={ p.Name }
}
data-tui-rating-onlyinteger={ strconv.FormatBool(p.OnlyInteger) }
class={
utils.TwMerge(
"flex flex-col items-start gap-1",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
if p.Name != "" {
<input
type="hidden"
name={ p.Name }
value={ fmt.Sprintf("%.2f", p.Value) }
if p.Form != "" {
form={ p.Form }
}
data-tui-rating-input
/>
}
</div>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-row items-center gap-1", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ p.setDefaults() }}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-rating-item
data-tui-rating-value={ strconv.Itoa(p.Value) }
class={
utils.TwMerge(
"relative",
colorClass(p.Style),
"transition-opacity",
"cursor-pointer", // Default cursor
p.Class,
),
}
{ p.Attributes... }
>
<div class="opacity-30">
@ratingIcon(p.Style, false, float64(p.Value))
</div>
<div
class="absolute inset-0 overflow-hidden w-0"
data-tui-rating-item-foreground
>
@ratingIcon(p.Style, true, float64(p.Value))
</div>
</div>
}
func colorClass(style Style) string {
switch style {
case StyleHeart:
return "text-destructive"
case StyleEmoji:
return "text-yellow-500"
default:
return "text-yellow-400"
}
}
func ratingIcon(style Style, filled bool, value float64) templ.Component {
if style == StyleEmoji {
if filled {
switch {
case value <= 1:
return icon.Angry()
case value <= 2:
return icon.Frown()
case value <= 3:
return icon.Meh()
case value <= 4:
return icon.Smile()
default:
return icon.Laugh()
}
}
return icon.Meh()
}
iconProps := icon.Props{}
if filled {
iconProps.Fill = "currentColor"
}
switch style {
case StyleHeart:
return icon.Heart(iconProps)
default:
return icon.Star(iconProps)
}
}
func (p *ItemProps) setDefaults() {
if p.Style == "" {
p.Style = StyleStar
}
}
func (p *Props) setDefaults() {
if p.Precision <= 0 {
p.Precision = 1.0
}
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/rating.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,325 @@
// templui component selectbox - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/select-box
package selectbox
import (
"context"
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type contextKey string
var contentIDKey contextKey = "contentID"
type Props struct {
ID string
Class string
Attributes templ.Attributes
Multiple bool
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Name string
Form string
Disabled bool
HasError bool
Multiple bool
ShowPills bool
SelectedCountText string
}
type ValueProps struct {
ID string
Class string
Attributes templ.Attributes
Placeholder string
Multiple bool
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
NoSearch bool
SearchPlaceholder string
}
type GroupProps struct {
ID string
Class string
Attributes templ.Attributes
}
type LabelProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ItemProps struct {
ID string
Class string
Attributes templ.Attributes
Value string
Selected bool
Disabled bool
}
templ SelectBox(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
wrapperID := p.ID
if wrapperID == "" {
wrapperID = utils.RandomID()
}
contentID := fmt.Sprintf("%s-content", wrapperID)
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
<div
id={ wrapperID }
class={ utils.TwMerge("select-container w-full relative", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
if p.ShowPills {
p.Multiple = true
}
}}
@popover.Trigger(popover.TriggerProps{
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
@button.Button(button.Props{
ID: p.ID,
Type: "button",
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Required class for JavaScript
"select-trigger",
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-selectbox-content-id": contentID,
"data-tui-selectbox-multiple": strconv.FormatBool(p.Multiple),
"data-tui-selectbox-show-pills": strconv.FormatBool(p.ShowPills),
"data-tui-selectbox-selected-count-text": p.SelectedCountText,
"tabindex": "0",
"aria-invalid": utils.If(p.HasError, "true"),
},
),
}) {
<input
type="hidden"
if p.Name != "" {
name={ p.Name }
}
if p.Form != "" {
form={ p.Form }
}
{ p.Attributes... }
/>
{ children... }
<span class="pointer-events-none ml-1">
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "text-muted-foreground",
})
</span>
}
}
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
if p.Placeholder != "" {
data-tui-selectbox-placeholder={ p.Placeholder }
}
{ p.Attributes... }
>
if p.Placeholder != "" {
{ p.Placeholder }
}
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{
var p ContentProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Offset: 4,
MatchWidth: true,
DisableESC: !p.NoSearch,
Class: utils.TwMerge(
"p-1 select-content z-50 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
"min-w-[var(--popover-trigger-width)] w-[var(--popover-trigger-width)]",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"role": "listbox",
"tabindex": "-1",
},
p.Attributes,
),
Exclusive: true,
}) {
if !p.NoSearch {
<div class="sticky top-0 bg-popover p-1">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground z-10 pointer-events-none">
@icon.Search(icon.Props{Size: 16})
</span>
@input.Input(input.Props{
Type: input.TypeSearch,
Class: "pl-8",
Placeholder: utils.IfElse(p.SearchPlaceholder != "", p.SearchPlaceholder, "Search..."),
Attributes: templ.Attributes{
"data-tui-selectbox-search": "",
},
})
</div>
</div>
}
<div class="max-h-[300px] overflow-y-auto">
{ children... }
</div>
}
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("p-1", p.Class) }
role="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Label(props ...LabelProps) {
{{ var p LabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-2 py-1.5 text-sm font-medium", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ Item(props ...ItemProps) {
{{ var p ItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"select-item relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-light outline-none",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground",
utils.If(p.Selected, "bg-accent text-accent-foreground"),
utils.If(p.Disabled, "pointer-events-none opacity-50"),
p.Class,
),
}
role="option"
data-tui-selectbox-value={ p.Value }
data-tui-selectbox-selected={ strconv.FormatBool(p.Selected) }
data-tui-selectbox-disabled={ strconv.FormatBool(p.Disabled) }
tabindex="0"
{ p.Attributes... }
>
<span class="truncate select-item-text">
{ children... }
</span>
<span
class={
utils.TwMerge(
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center",
utils.IfElse(p.Selected, "opacity-100", "opacity-0"),
),
}
>
@icon.Check(icon.Props{Size: 16})
</span>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/selectbox.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,98 @@
// templui component separator - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/separator
package separator
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Orientation string
type Decoration string
const (
OrientationHorizontal Orientation = "horizontal"
OrientationVertical Orientation = "vertical"
)
const (
DecorationDashed Decoration = "dashed"
DecorationDotted Decoration = "dotted"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Orientation Orientation
Decoration Decoration
}
templ Separator(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Orientation == "" {
{{ p.Orientation = OrientationHorizontal }}
}
if p.Orientation == OrientationHorizontal {
<div
if p.ID != "" {
id={ p.ID }
}
role="separator"
aria-orientation="horizontal"
class={ utils.TwMerge("shrink-0 w-full", p.Class) }
{ p.Attributes... }
>
<div class="relative flex items-center w-full">
<span
class={
utils.TwMerge(
"absolute w-full border-t h-[1px]",
decorationClasses(p.Decoration),
),
}
aria-hidden="true"
></span>
<span class="relative mx-auto bg-background px-2 text-xs text-muted-foreground">
{ children... }
</span>
</div>
</div>
} else {
<div
if p.ID != "" {
id={ p.ID }
}
role="separator"
aria-orientation="vertical"
class={ utils.TwMerge("shrink-0 h-full", p.Class) }
{ p.Attributes... }
>
<div class="relative flex flex-col items-center h-full">
<span
class={
utils.TwMerge(
"absolute h-full border-l w-[1px]",
decorationClasses(p.Decoration),
),
}
aria-hidden="true"
></span>
<span class="relative my-auto bg-background py-2 text-xs text-muted-foreground">
{ children... }
</span>
</div>
</div>
}
}
func decorationClasses(decoration Decoration) string {
switch decoration {
case DecorationDashed:
return "border-dashed"
case DecorationDotted:
return "border-dotted"
default:
return ""
}
}

View file

@ -0,0 +1,318 @@
// templui component sheet - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/sheet
package sheet
import (
"context"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/dialog"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type contextKey string
const (
sideKey contextKey = "sheetSide"
)
type Side string
const (
SideTop Side = "top"
SideRight Side = "right"
SideBottom Side = "bottom"
SideLeft Side = "left"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Side Side
Open bool
DisableClickAway bool
DisableESC bool
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Reference to a specific sheet ID (for external triggers)
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
HideCloseButton bool
Side Side //
Open bool // Initial open state for standalone usage
}
type HeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type FooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TitleProps struct {
ID string
Class string
Attributes templ.Attributes
}
type DescriptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CloseProps struct {
ID string
Class string
Attributes templ.Attributes
For string
}
templ Sheet(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Side == "" {
{{ p.Side = SideRight }}
}
// Pass the Side through context to child components
{{ ctx = context.WithValue(ctx, sideKey, p.Side) }}
// Sheet uses Dialog internally with sheet-specific attributes
@dialog.Dialog(dialog.Props{
ID: p.ID,
Open: p.Open,
DisableClickAway: p.DisableClickAway,
DisableESC: p.DisableESC,
Class: p.Class,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-sheet": "true",
"data-tui-sheet-side": string(p.Side),
},
p.Attributes,
),
}) {
{ children... }
}
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Sheet trigger is just a Dialog trigger
@dialog.Trigger(dialog.TriggerProps{
ID: p.ID,
For: p.For,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Get Side from context if not explicitly provided
if p.Side == "" {
if val := ctx.Value(sideKey); val != nil {
{{ p.Side = val.(Side) }}
} else {
{{ p.Side = SideRight }}
}
}
// Sheet content uses Dialog content with sheet-specific styles
@dialog.Content(dialog.ContentProps{
ID: p.ID,
Open: p.Open,
HideCloseButton: p.HideCloseButton,
Class: utils.TwMerge(
// First apply side-specific positioning and animations
getSideClasses(p.Side),
// Default gap matching shadcn (no padding in content)
"gap-4 !p-0", // Remove Dialog's p-6 padding
// Override Dialog styles
"!scale-100", // Reset Dialog's scale animation
"!rounded-none", // Remove dialog rounded corners
"!opacity-100", // Keep fully opaque - no fade, only slide
// Remove pointer-events control during animation
"!pointer-events-auto data-[tui-dialog-hidden=true]:!pointer-events-none",
// User-provided classes last
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-sheet-content": "true",
"data-tui-sheet-side": string(p.Side),
},
p.Attributes,
),
}) {
{ children... }
}
}
templ Header(props ...HeaderProps) {
{{ var p HeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Sheet header uses Dialog header but overrides styles
@dialog.Header(dialog.HeaderProps{
ID: p.ID,
Class: utils.TwMerge("gap-1.5 p-4 text-left", p.Class),
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Title(props ...TitleProps) {
{{ var p TitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Sheet title uses Dialog title but overrides styles
@dialog.Title(dialog.TitleProps{
ID: p.ID,
Class: utils.TwMerge("text-base leading-normal", p.Class),
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Description(props ...DescriptionProps) {
{{ var p DescriptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Sheet description uses Dialog description
@dialog.Description(dialog.DescriptionProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Footer(props ...FooterProps) {
{{ var p FooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Sheet footer uses Dialog footer but overrides styles
@dialog.Footer(dialog.FooterProps{
ID: p.ID,
Class: utils.TwMerge("mt-auto flex flex-col gap-2 p-4", p.Class),
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Close(props ...CloseProps) {
{{ var p CloseProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Sheet close uses Dialog close
@dialog.Close(dialog.CloseProps{
ID: p.ID,
For: p.For,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
}
func getSideClasses(side Side) string {
// Base classes for all sheets - matching shadcn
// Duration varies: 300ms when closing, 500ms when opening
// Use !transition-transform to override Dialog's transition-all
baseClasses := "fixed z-50 flex flex-col bg-background shadow-lg !transition-transform ease-in-out " +
"data-[tui-dialog-open=false]:duration-300 data-[tui-dialog-open=true]:duration-500 "
switch side {
case SideRight:
return baseClasses +
// Positioning
"!inset-y-0 !right-0 !left-auto !top-auto " +
// Size
"h-full w-3/4 sm:max-w-sm " +
// Border
"border-l border-t-0 border-r-0 border-b-0 " +
// Reset Dialog transforms
"!translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
case SideLeft:
return baseClasses +
// Positioning
"!inset-y-0 !left-0 !right-auto !top-auto " +
// Size
"h-full w-3/4 sm:max-w-sm " +
// Border
"border-r border-t-0 border-l-0 border-b-0 " +
// Reset Dialog transforms
"!translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!-translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
case SideTop:
return baseClasses +
// Positioning - full width at top
"!inset-x-0 !top-0 !bottom-auto !left-0 !right-0 " +
// Size - full width, auto height
"!w-full !max-w-full h-auto " +
// Border
"border-b border-t-0 border-l-0 border-r-0 " +
// Reset Dialog transforms - IMPORTANT: reset the centering from Dialog
"!translate-x-0 !translate-y-0 " +
// Slide animation - top slides up when closing
"data-[tui-dialog-open=false]:!-translate-y-full " +
"data-[tui-dialog-open=true]:!translate-y-0"
case SideBottom:
return baseClasses +
// Positioning - full width at bottom
"!inset-x-0 !bottom-0 !top-auto !left-0 !right-0 " +
// Size - full width, auto height
"!w-full !max-w-full h-auto " +
// Border
"border-t border-b-0 border-l-0 border-r-0 " +
// Reset Dialog transforms - IMPORTANT: reset the centering from Dialog
"!translate-x-0 !translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!translate-y-full " +
"data-[tui-dialog-open=true]:!translate-y-0"
default:
return baseClasses +
// Default to right side
"!inset-y-0 !right-0 !left-auto !top-auto " +
"h-full w-3/4 " +
"border-l border-t-0 border-r-0 border-b-0 " +
"!translate-y-0 " +
"data-[tui-dialog-open=false]:!translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
}
}

View file

@ -0,0 +1,753 @@
// templui component sidebar - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/sidebar
package sidebar
import "context"
import "git.juancwu.dev/juancwu/budgething/internal/utils"
import "git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
import "git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
import "git.juancwu.dev/juancwu/budgething/internal/ui/components/sheet"
import "git.juancwu.dev/juancwu/budgething/internal/ui/components/tooltip"
type contextKey string
const sidebarIDKey contextKey = "sidebar-id"
type Side string
const (
SideLeft Side = "left" // default
SideRight Side = "right"
)
type Variant string
const (
VariantSidebar Variant = "sidebar" // default
VariantFloating Variant = "floating"
VariantInset Variant = "inset"
)
type Collapsible string
const (
CollapsibleOffcanvas Collapsible = "offcanvas" // default
CollapsibleIcon Collapsible = "icon"
CollapsibleNone Collapsible = "none"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Side Side // default: "left"
Variant Variant // default: "sidebar"
Collapsible Collapsible // default: "offcanvas"
Collapsed bool // default: false (sidebar open)
KeyboardShortcut string // default: "b"
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Target string // Target sidebar ID to toggle
}
type HeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
type FooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
type InsetProps struct {
ID string
Class string
Attributes templ.Attributes
}
type GroupProps struct {
ID string
Class string
Attributes templ.Attributes
}
type GroupLabelProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MenuProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MenuItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MenuButtonSize string
const (
MenuButtonSizeDefault MenuButtonSize = "default" // default
MenuButtonSizeSm MenuButtonSize = "sm"
MenuButtonSizeLg MenuButtonSize = "lg"
)
type MenuButtonProps struct {
ID string
Class string
Attributes templ.Attributes
Href string
IsActive bool
Size MenuButtonSize // default: "default"
Tooltip string // Tooltip text to show when sidebar is collapsed
}
type MenuBadgeProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MenuSubProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MenuSubItemProps struct {
ID string
Class string
Attributes templ.Attributes
}
type MenuSubButtonProps struct {
ID string
Class string
Attributes templ.Attributes
Href string
IsActive bool
}
type SeparatorProps struct {
ID string
Class string
Attributes templ.Attributes
}
type LayoutProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Layout(props ...LayoutProps) {
{{ var p LayoutProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Generate ID for context (children components can use it for targeting)
{{ var sidebarId string = p.ID }}
if sidebarId == "" {
{{ sidebarId = utils.RandomID() }}
}
// Set sidebar ID in context for children to access
{{ ctx = context.WithValue(ctx, sidebarIDKey, sidebarId) }}
<!-- Layout Container - No ID needed here -->
<div
class={ utils.TwMerge(
"flex min-h-svh relative",
"has-[[data-tui-sidebar-variant=inset]]:bg-sidebar",
p.Class,
) }
data-tui-sidebar-layout
{ p.Attributes... }
>
{ children... }
</div>
}
templ Sidebar(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Get sidebar ID from context or use provided/generate new
if p.ID == "" {
if ctxId := ctx.Value(sidebarIDKey); ctxId != nil {
{{ p.ID = ctxId.(string) }}
} else {
{{ p.ID = utils.RandomID() }}
}
}
if p.Side == "" {
{{ p.Side = SideLeft }}
}
if p.Variant == "" {
{{ p.Variant = VariantSidebar }}
}
if p.Collapsible == "" {
{{ p.Collapsible = CollapsibleOffcanvas }}
}
if p.KeyboardShortcut == "" {
{{ p.KeyboardShortcut = "b" }}
}
// Use the sidebar's ID for mobile sheet and trigger targeting
{{ var sidebarId string = p.ID }}
{{ sidebarState := "expanded" }}
if p.Collapsed {
{{ sidebarState = "collapsed" }}
}
<!-- Mobile: Sheet Component for < 768px -->
{{ var sheetSide sheet.Side }}
if p.Side == SideRight {
{{ sheetSide = sheet.SideRight }}
} else {
{{ sheetSide = sheet.SideLeft }}
}
<!-- Mobile sheet wrapper without content -->
@sheet.Sheet(sheet.Props{
ID: sidebarId + "-mobile",
Side: sheetSide,
Open: false,
}) {
@sheet.Content(sheet.ContentProps{
Class: "md:hidden bg-sidebar text-sidebar-foreground !p-0 !gap-0 flex flex-col h-full",
HideCloseButton: true,
}) {
<!-- Mobile content wrapper: visible only on mobile via JS -->
<div class="sidebar-mobile-portal flex h-full flex-col" data-tui-sidebar-mobile-portal={ sidebarId }></div>
}
}
<!-- Desktop: Sidebar for >= 768px -->
<div
class={ utils.TwMerge(
"group peer hidden md:block",
p.Class,
) }
data-tui-sidebar-state={ sidebarState }
data-tui-sidebar-collapsible={ string(p.Collapsible) }
data-tui-sidebar-variant={ string(p.Variant) }
data-tui-sidebar-side={ string(p.Side) }
data-tui-sidebar-wrapper
data-tui-sidebar-id={ p.ID }
data-tui-sidebar-keyboard-shortcut={ p.KeyboardShortcut }
{ utils.MergeAttributes(
templ.Attributes{"style": "--sidebar-width:16rem"},
p.Attributes,
)... }
>
<!-- Gap element for document flow -->
<div
class={ utils.TwMerge(
"relative bg-transparent transition-[width] duration-200 ease-linear",
"w-[var(--sidebar-width,16rem)]",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=offcanvas]:w-0",
"group-data-[tui-sidebar-side=right]:rotate-180",
// Add padding for floating/inset variants when collapsed to icon mode
"group-data-[tui-sidebar-variant=floating]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:w-[calc(3rem+theme(spacing.4))]",
"group-data-[tui-sidebar-variant=inset]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:w-[calc(3rem+theme(spacing.4))]",
"group-data-[tui-sidebar-variant=sidebar]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:w-12",
) }
></div>
<!-- Sidebar Container -->
<aside
id={ p.ID }
class={ utils.TwMerge(
"fixed inset-y-0 z-10 hidden h-svh transition-transform duration-200 ease-linear md:flex",
"w-[var(--sidebar-width,16rem)]",
// Side positioning with data attributes
"group-data-[tui-sidebar-side=right]:right-0 group-data-[tui-sidebar-side=right]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=offcanvas]:translate-x-full",
"group-data-[tui-sidebar-side=left]:left-0 group-data-[tui-sidebar-side=left]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=offcanvas]:-translate-x-full",
// Adjust padding and width for variants
"group-data-[tui-sidebar-variant=floating]:p-2 group-data-[tui-sidebar-variant=floating]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:w-[calc(3rem+(theme(spacing.4))+2px)]",
"group-data-[tui-sidebar-variant=inset]:p-2 group-data-[tui-sidebar-variant=inset]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:w-[calc(3rem+(theme(spacing.4))+2px)]",
"group-data-[tui-sidebar-variant=sidebar]:group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:w-12",
"group-data-[tui-sidebar-variant=sidebar]:group-data-[tui-sidebar-side=left]:border-r group-data-[tui-sidebar-variant=sidebar]:group-data-[tui-sidebar-side=right]:border-l",
p.Class,
) }
data-sidebar="sidebar"
>
<!-- Inner sidebar with variant-specific styling -->
<div
data-sidebar="sidebar"
class={ utils.TwMerge(
"bg-sidebar group-data-[tui-sidebar-variant=floating]:border-sidebar-border flex h-full w-full flex-col",
"group-data-[tui-sidebar-variant=floating]:rounded-lg group-data-[tui-sidebar-variant=floating]:border group-data-[tui-sidebar-variant=floating]:shadow-sm",
) }
>
<!-- Main content: rendered once, shown conditionally -->
<div data-tui-sidebar-content={ sidebarId } class="flex h-full w-full flex-col sidebar-content">
{ children... }
</div>
</div>
</aside>
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
// Get sidebar ID from: 1. Target prop, 2. Context
{{ var sidebarId string }}
if p.Target != "" {
{{ sidebarId = p.Target }}
} else if ctxId := ctx.Value(sidebarIDKey); ctxId != nil {
{{ sidebarId = ctxId.(string) }}
}
<!-- Mobile: Trigger to open the sheet created in Sidebar component -->
@sheet.Trigger(sheet.TriggerProps{
For: sidebarId + "-mobile",
Class: "md:hidden",
}) {
@button.Button(button.Props{
Size: button.SizeIcon,
Variant: button.VariantGhost,
Class: "size-7",
}) {
@icon.PanelLeft(icon.Props{Class: "size-4"})
<span class="sr-only">Toggle Sidebar</span>
}
}
<!-- Desktop: Sidebar Trigger -->
<div class="hidden md:block">
@button.Button(button.Props{
Size: button.SizeIcon,
Variant: button.VariantGhost,
Class: utils.TwMerge(
"size-7",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-sidebar-trigger": true,
"data-tui-sidebar-target": sidebarId,
},
p.Attributes,
)},
) {
@icon.PanelLeft(icon.Props{Class: "size-4"})
<span class="sr-only">Toggle Sidebar</span>
}
</div>
}
templ Header(props ...HeaderProps) {
{{ var p HeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col gap-2 p-2", p.Class) }
data-tui-sidebar="header"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Footer(props ...FooterProps) {
{{ var p FooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("mt-auto flex flex-col gap-2 p-2", p.Class) }
data-tui-sidebar="footer"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:overflow-hidden",
p.Class,
) }
data-tui-sidebar="content"
{ p.Attributes... }
>
{ children... }
</div>
}
templ Menu(props ...MenuProps) {
{{ var p MenuProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<ul
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex w-full min-w-0 flex-col gap-1", p.Class) }
data-tui-sidebar="menu"
{ p.Attributes... }
>
{ children... }
</ul>
}
templ MenuItem(props ...MenuItemProps) {
{{ var p MenuItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<li
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("group/menu-item relative", p.Class) }
data-tui-sidebar="menu-item"
{ p.Attributes... }
>
{ children... }
</li>
}
templ MenuButton(props ...MenuButtonProps) {
{{ var p MenuButtonProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Size == "" {
{{ p.Size = MenuButtonSizeDefault }}
}
if p.Tooltip != "" {
{{ tooltipID := utils.RandomID() }}
// When collapsed to icon mode - show with tooltip
<div class="group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:block hidden">
@tooltip.Tooltip() {
@tooltip.Trigger(tooltip.TriggerProps{
For: tooltipID,
}) {
@menuButtonContent(p, "") {
{ children... }
}
}
@tooltip.Content(tooltip.ContentProps{
ID: tooltipID,
Position: tooltip.PositionRight,
HoverDelay: 200,
HoverOutDelay: 100,
}) {
{ p.Tooltip }
}
}
</div>
// When expanded - show without tooltip
<div class="group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden">
@menuButtonContent(p, "") {
{ children... }
}
</div>
} else {
@menuButtonContent(p, "") {
{ children... }
}
}
}
templ menuButtonContent(p MenuButtonProps, buttonID string) {
if p.Href != "" {
<a
if buttonID != "" {
id={ buttonID }
}
href={ templ.SafeURL(p.Href) }
class={ utils.TwMerge(
"flex w-full items-center gap-2 rounded-md p-2 text-left overflow-hidden",
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
"transition-[width,height,padding]",
// Size variants with data attributes
"data-[tui-sidebar-size=sm]:h-7 data-[tui-sidebar-size=sm]:text-xs",
"data-[tui-sidebar-size=lg]:h-12 data-[tui-sidebar-size=lg]:text-sm",
"data-[tui-sidebar-size=default]:h-8 data-[tui-sidebar-size=default]:text-sm",
// Active state with data attributes
"data-[tui-sidebar-active=true]:bg-sidebar-accent data-[tui-sidebar-active=true]:text-sidebar-accent-foreground data-[tui-sidebar-active=true]:font-medium",
// Collapsed icon mode styles - matching shadcn exactly
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:!size-8",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:!p-2",
// Override padding for lg size (avatars) in collapsed mode
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:data-[tui-sidebar-size=lg]:!p-0",
"[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
p.Class,
) }
data-tui-sidebar="menu-button"
data-tui-sidebar-size={ string(p.Size) }
if p.IsActive {
data-tui-sidebar-active="true"
}
{ p.Attributes... }
>
{ children... }
</a>
} else {
<button
if buttonID != "" {
id={ buttonID }
}
type="button"
class={ utils.TwMerge(
"flex w-full items-center gap-2 rounded-md p-2 text-left overflow-hidden",
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
"transition-[width,height,padding]",
// Size variants with data attributes
"data-[tui-sidebar-size=sm]:h-7 data-[tui-sidebar-size=sm]:text-xs",
"data-[tui-sidebar-size=lg]:h-12 data-[tui-sidebar-size=lg]:text-sm",
"data-[tui-sidebar-size=default]:h-8 data-[tui-sidebar-size=default]:text-sm",
// Active state with data attributes
"data-[tui-sidebar-active=true]:bg-sidebar-accent data-[tui-sidebar-active=true]:text-sidebar-accent-foreground data-[tui-sidebar-active=true]:font-medium",
// Collapsed icon mode styles - matching shadcn exactly
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:!size-8",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:!p-2",
// Override padding for lg size (avatars) in collapsed mode
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:data-[tui-sidebar-size=lg]:!p-0",
"[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
p.Class,
) }
data-tui-sidebar="menu-button"
data-tui-sidebar-size={ string(p.Size) }
if p.IsActive {
data-tui-sidebar-active="true"
}
{ p.Attributes... }
>
{ children... }
</button>
}
}
templ MenuSub(props ...MenuSubProps) {
{{ var p MenuSubProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<ul
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden",
p.Class,
) }
data-tui-sidebar="menu-sub"
{ p.Attributes... }
>
{ children... }
</ul>
}
templ MenuSubItem(props ...MenuSubItemProps) {
{{ var p MenuSubItemProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<li
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("group/menu-sub-item relative", p.Class) }
data-tui-sidebar="menu-sub-item"
{ p.Attributes... }
>
{ children... }
</li>
}
templ MenuSubButton(props ...MenuSubButtonProps) {
{{ var p MenuSubButtonProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Href != "" {
<a
if p.ID != "" {
id={ p.ID }
}
href={ templ.SafeURL(p.Href) }
class={ utils.TwMerge(
"flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
"transition-colors",
"data-[tui-sidebar-active=true]:bg-sidebar-accent data-[tui-sidebar-active=true]:text-sidebar-accent-foreground data-[tui-sidebar-active=true]:font-medium",
p.Class,
) }
data-tui-sidebar="menu-sub-button"
if p.IsActive {
data-tui-sidebar-active="true"
}
{ p.Attributes... }
>
{ children... }
</a>
} else {
<button
if p.ID != "" {
id={ p.ID }
}
type="button"
class={ utils.TwMerge(
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-left",
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
"transition-colors",
"data-[tui-sidebar-active=true]:bg-sidebar-accent data-[tui-sidebar-active=true]:text-sidebar-accent-foreground data-[tui-sidebar-active=true]:font-medium",
p.Class,
) }
data-tui-sidebar="menu-sub-button"
if p.IsActive {
data-tui-sidebar-active="true"
}
{ p.Attributes... }
>
{ children... }
</button>
}
}
templ Inset(props ...InsetProps) {
{{ var p InsetProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<main
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"relative flex w-full flex-1 flex-col bg-background",
// Add special styling when peer sidebar has variant="inset"
"md:peer-data-[tui-sidebar-variant=inset]:m-2",
"md:peer-data-[tui-sidebar-variant=inset]:ml-0",
"md:peer-data-[tui-sidebar-variant=inset]:rounded-xl",
"md:peer-data-[tui-sidebar-variant=inset]:shadow-sm",
// When sidebar is collapsed (offcanvas mode) and variant is inset, add left margin back
"md:peer-data-[tui-sidebar-variant=inset]:peer-data-[tui-sidebar-state=collapsed]:peer-data-[tui-sidebar-collapsible=offcanvas]:ml-2",
p.Class,
) }
data-tui-sidebar="inset"
{ p.Attributes... }
>
{ children... }
</main>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("relative flex w-full min-w-0 flex-col p-2", p.Class) }
data-tui-sidebar="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ GroupLabel(props ...GroupLabelProps) {
{{ var p GroupLabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70",
"ring-sidebar-ring outline-none transition-[margin,opacity] duration-200 ease-linear",
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:-mt-8 group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:opacity-0",
p.Class,
) }
data-tui-sidebar="group-label"
{ p.Attributes... }
>
{ children... }
</div>
}
templ MenuBadge(props ...MenuBadgeProps) {
{{ var p MenuBadgeProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"ml-auto flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium",
"bg-sidebar-accent text-sidebar-accent-foreground",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden",
p.Class,
) }
data-tui-sidebar="menu-badge"
{ p.Attributes... }
>
{ children... }
</span>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<hr
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"mx-2 my-2 border-t border-sidebar-border",
p.Class,
) }
data-tui-sidebar="separator"
{ p.Attributes... }
/>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/sidebar.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,30 @@
// templui component skeleton - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/skeleton
package skeleton
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
templ Skeleton(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"animate-pulse rounded bg-muted",
p.Class,
),
}
{ p.Attributes... }
></div>
}

View file

@ -0,0 +1,121 @@
// templui component slider - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/slider
package slider
import (
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type InputProps struct {
ID string
Class string
Attributes templ.Attributes
Name string
Min int
Max int
Step int
Value int
Disabled bool
}
type ValueProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Corresponds to the ID of the Slider Input
}
templ Slider(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("w-full", p.Class) }
data-tui-slider-wrapper
{ p.Attributes... }
>
{ children... }
</div>
}
templ Input(props ...InputProps) {
{{ var p InputProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<input
type="range"
id={ p.ID }
data-tui-slider-input
if p.Name != "" {
name={ p.Name }
}
if p.Value != 0 {
value={ fmt.Sprintf("%d", p.Value) }
}
if p.Min != 0 {
min={ fmt.Sprintf("%d", p.Min) }
}
if p.Max != 0 {
max={ fmt.Sprintf("%d", p.Max) }
}
if p.Step != 0 {
step={ fmt.Sprintf("%d", p.Step) }
}
class={
utils.TwMerge(
"w-full h-2 rounded-full bg-secondary appearance-none cursor-pointer",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4",
"[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary",
"[&::-webkit-slider-thumb]:hover:bg-primary/90",
"[&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:border-0",
"[&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary",
"[&::-moz-range-thumb]:hover:bg-primary/90",
"disabled:opacity-50 disabled:cursor-not-allowed",
p.Class,
),
}
disabled?={ p.Disabled }
{ p.Attributes... }
/>
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.For == "" {
<span class="text-xs text-destructive">Error: SliderValue missing 'For' attribute.</span>
}
<span
if p.ID != "" {
id={ p.ID }
}
data-tui-slider-value
data-tui-slider-value-for={ p.For }
class={ utils.TwMerge("text-sm text-muted-foreground", p.Class) }
{ p.Attributes... }
>
<!-- Initial value will be set by JS -->
</span>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/slider.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,86 @@
// templui component switch - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/switch
package switchcomp
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Value string
Disabled bool
Checked bool
Form string
}
templ Switch(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<label
for={ p.ID }
class={ utils.TwMerge(
"inline-flex cursor-pointer items-center gap-2",
utils.If(p.Disabled, "cursor-not-allowed"),
) }
>
<!-- Actual checkbox switch -->
<input
id={ p.ID }
if p.Name != "" {
name={ p.Name }
}
type="checkbox"
if p.Value != "" {
value={ p.Value }
} else {
value="on"
}
if p.Form != "" {
form={ p.Form }
}
checked?={ p.Checked }
disabled?={ p.Disabled }
class="peer hidden"
role="switch"
{ p.Attributes... }
/>
<!-- Visual switch UI -->
<div
class={
utils.TwMerge(
// Container
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center",
"rounded-full border-2 border-transparent",
"transition-colors",
// Background colors
"bg-input",
"peer-checked:bg-primary",
// Focus styles
"peer-focus-visible:outline-none peer-focus-visible:ring-2",
"peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2",
"peer-focus-visible:ring-offset-background",
// Disabled state
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
// Thumb
"after:pointer-events-none after:block",
"after:h-4 after:w-4",
"after:rounded-full after:bg-background",
"after:shadow-lg after:ring-0",
"after:transition-transform",
"after:content-['']",
// Thumb position
"peer-checked:after:translate-x-4",
p.Class,
),
}
aria-hidden="true"
></div>
</label>
}

View file

@ -0,0 +1,205 @@
// templui component table - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/table
package table
import "git.juancwu.dev/juancwu/budgething/internal/utils"
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type HeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type BodyProps struct {
ID string
Class string
Attributes templ.Attributes
}
type FooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
type RowProps struct {
ID string
Class string
Attributes templ.Attributes
Selected bool
}
type HeadProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CellProps struct {
ID string
Class string
Attributes templ.Attributes
}
type CaptionProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Table(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div class="relative w-full overflow-auto">
<table
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("w-full caption-bottom text-sm", p.Class) }
{ p.Attributes... }
>
{ children... }
</table>
</div>
}
templ Header(props ...HeaderProps) {
{{ var p HeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<thead
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("[&_tr]:border-b", p.Class) }
{ p.Attributes... }
>
{ children... }
</thead>
}
templ Body(props ...BodyProps) {
{{ var p BodyProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<tbody
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("[&_tr:last-child]:border-0", p.Class) }
{ p.Attributes... }
>
{ children... }
</tbody>
}
templ Footer(props ...FooterProps) {
{{ var p FooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<tfoot
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", p.Class) }
{ p.Attributes... }
>
{ children... }
</tfoot>
}
templ Row(props ...RowProps) {
{{ var p RowProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<tr
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"border-b transition-colors hover:bg-muted/50",
utils.If(p.Selected, "data-[tui-table-state-selected]:bg-muted"),
p.Class,
),
}
if p.Selected {
data-tui-table-state-selected
}
{ p.Attributes... }
>
{ children... }
</tr>
}
templ Head(props ...HeadProps) {
{{ var p HeadProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<th
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground",
"[&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</th>
}
templ Cell(props ...CellProps) {
{{ var p CellProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<td
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"p-2 align-middle",
"[&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</td>
}
templ Caption(props ...CaptionProps) {
{{ var p CaptionProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<caption
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("mt-4 text-sm text-muted-foreground", p.Class) }
{ p.Attributes... }
>
{ children... }
</caption>
}

View file

@ -0,0 +1,163 @@
// templui component tabs - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/tabs
package tabs
import (
"context"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type ListProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Value string
IsActive bool
TabsID string
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
Value string
IsActive bool
TabsID string
}
templ Tabs(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := p.ID }}
if tabsID == "" {
{{ tabsID = utils.RandomID() }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("flex flex-col gap-2", p.Class) }
data-tui-tabs
data-tui-tabs-id={ tabsID }
{ p.Attributes... }
>
{{ ctx = context.WithValue(ctx, "tabsId", tabsID) }}
{ children... }
</div>
}
templ List(props ...ListProps) {
{{ var p ListProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := IDFromContext(ctx) }}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
p.Class,
),
}
data-tui-tabs-list
data-tui-tabs-id={ tabsID }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := p.TabsID }}
if tabsID == "" {
{{ tabsID = IDFromContext(ctx) }}
}
if p.Value == "" {
<span class="text-xs text-destructive">Error: Tab Trigger missing required 'Value' attribute.</span>
}
<button
if p.ID != "" {
id={ p.ID }
}
type="button"
class={
utils.TwMerge(
"data-[tui-tabs-state=active]:bg-background dark:data-[tui-tabs-state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[tui-tabs-state=active]:border-input dark:data-[tui-tabs-state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[tui-tabs-state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
p.Class,
),
}
data-tui-tabs-trigger
data-tui-tabs-id={ tabsID }
data-tui-tabs-value={ p.Value }
data-tui-tabs-state={ utils.IfElse(p.IsActive, "active", "inactive") }
{ p.Attributes... }
>
{ children... }
</button>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := p.TabsID }}
if tabsID == "" {
{{ tabsID = IDFromContext(ctx) }}
}
if p.Value == "" {
<span class="text-xs text-destructive">Error: Tab Content missing required 'Value' attribute.</span>
return templ.NopComponent
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"flex-1 outline-none",
utils.If(!p.IsActive, "hidden"),
p.Class,
),
}
data-tui-tabs-content
data-tui-tabs-id={ tabsID }
data-tui-tabs-value={ p.Value }
data-tui-tabs-state={ utils.IfElse(p.IsActive, "active", "inactive") }
{ p.Attributes... }
>
{ children... }
</div>
}
func IDFromContext(ctx context.Context) string {
if tabsID, ok := ctx.Value("tabsId").(string); ok {
return tabsID
}
return ""
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/tabs.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,94 @@
// templui component tagsinput - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/tags-input
package tagsinput
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Props struct {
ID string
Name string
Value []string
Form string
Placeholder string
Class string
HasError bool
Attributes templ.Attributes
Disabled bool
Readonly bool
}
templ TagsInput(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
id={ p.ID + "-container" }
class={
utils.TwMerge(
// Base styles
"flex items-center flex-wrap gap-2 p-2 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Focus styles
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
// Disabled styles
utils.If(p.Disabled, "opacity-50 cursor-not-allowed"),
// Width
"w-full",
// Error/Invalid styles
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
}
data-tui-tagsinput
data-tui-tagsinput-name={ p.Name }
data-tui-tagsinput-form={ p.Form }
{ p.Attributes... }
>
<div class="flex items-center flex-wrap gap-2" data-tui-tagsinput-container>
for _, tag := range p.Value {
@badge.Badge(badge.Props{
Attributes: templ.Attributes{"data-tui-tagsinput-chip": ""},
}) {
<span>{ tag }</span>
<button
type="button"
class="ml-1 text-current hover:text-destructive disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
disabled?={ p.Disabled }
data-tui-tagsinput-remove
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
}
}
</div>
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
<div data-tui-tagsinput-hidden-inputs>
for _, tag := range p.Value {
<input type="hidden" name={ p.Name } value={ tag }/>
}
</div>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/tagsinput.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,85 @@
// templui component textarea - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/textarea
package textarea
import (
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Value string
Form string
Placeholder string
Rows int
AutoResize bool
Disabled bool
Readonly bool
HasError bool
}
templ Textarea(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<textarea
id={ p.ID }
data-tui-textarea
if p.Name != "" {
name={ p.Name }
}
if p.Form != "" {
form={ p.Form }
}
if p.Placeholder != "" {
placeholder={ p.Placeholder }
}
if p.Rows > 0 {
rows={ strconv.Itoa(p.Rows) }
}
disabled?={ p.Disabled }
readonly?={ p.Readonly }
if p.HasError {
aria-invalid="true"
}
if p.AutoResize {
data-tui-textarea-auto-resize="true"
}
class={
utils.TwMerge(
// Base styles
"flex w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
"min-h-[80px]", // Default min-height
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Placeholder
"placeholder:text-muted-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Disabled styles
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
// Add overflow-hidden only if auto-resizing to prevent scrollbar flicker
utils.If(p.AutoResize, "overflow-hidden resize-none"),
p.Class,
),
}
{ p.Attributes... }
>{ p.Value }</textarea>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/textarea.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,250 @@
// templui component timepicker - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/time-picker
package timepicker
import (
"fmt"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/card"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
"time"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Name string
Form string
Value time.Time
MinTime time.Time
MaxTime time.Time
Step int
Use12Hours bool
AMLabel string
PMLabel string
Placeholder string
Disabled bool
HasError bool
}
templ TimePicker(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
if p.ID == "" {
p.ID = utils.RandomID()
}
if p.Name == "" {
p.Name = p.ID
}
if p.Placeholder == "" {
p.Placeholder = "Select time"
}
if p.AMLabel == "" {
p.AMLabel = "AM"
}
if p.PMLabel == "" {
p.PMLabel = "PM"
}
if p.Step <= 0 {
p.Step = 1
}
var contentID = p.ID + "-content"
var valueString string
if p.Value != (time.Time{}) {
valueString = p.Value.Format("15:04")
}
var minTimeString string
if p.MinTime != (time.Time{}) {
minTimeString = p.MinTime.Format("15:04")
}
var maxTimeString string
if p.MaxTime != (time.Time{}) {
maxTimeString = p.MaxTime.Format("15:04")
}
}}
<div class="relative inline-block w-full">
<input
type="hidden"
name={ p.Name }
value={ valueString }
if p.Form != "" {
form={ p.Form }
}
id={ p.ID + "-hidden" }
data-tui-timepicker-hidden-input="true"
/>
@popover.Trigger(popover.TriggerProps{For: contentID}) {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-timepicker": "true",
"data-tui-timepicker-use12hours": fmt.Sprintf("%t", p.Use12Hours),
"data-tui-timepicker-am-label": p.AMLabel,
"data-tui-timepicker-pm-label": p.PMLabel,
"data-tui-timepicker-placeholder": p.Placeholder,
"data-tui-timepicker-step": fmt.Sprintf("%d", p.Step),
"data-tui-timepicker-min-time": minTimeString,
"data-tui-timepicker-max-time": maxTimeString,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
<span data-tui-timepicker-display class="text-left grow text-muted-foreground">
{ p.Placeholder }
</span>
<span class="text-muted-foreground flex items-center ml-2">
@icon.Clock(icon.Props{Size: 16})
</span>
}
}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Class: "p-0 w-80",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@card.Content(card.ContentProps{
Class: "p-4",
}) {
<div
data-tui-timepicker-popup="true"
data-tui-timepicker-input-name={ p.Name }
data-tui-timepicker-parent-id={ p.ID }
if valueString != "" {
data-tui-timepicker-value={ valueString }
}
>
// Time selection grid
<div class="grid grid-cols-2 gap-3 mb-4">
// Hour selection
<div class="space-y-2">
<label class="text-sm font-medium">Hour</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-hour-list="true" class="p-1 space-y-0.5">
if p.Use12Hours {
// 12-hour format: 12, 01-11
<button
type="button"
data-tui-timepicker-hour="0"
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
12
</button>
for hour := 1; hour <= 11; hour++ {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
</button>
}
} else {
// 24-hour format: 00-23
for hour := 0; hour < 24; hour++ {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
</button>
}
}
</div>
</div>
</div>
// Minute selection
<div class="space-y-2">
<label class="text-sm font-medium">Minute</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-minute-list="true" class="p-1 space-y-0.5">
for minute := 0; minute < 60; minute += p.Step {
<button
type="button"
data-tui-timepicker-minute={ strconv.Itoa(minute) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", minute) }
</button>
}
</div>
</div>
</div>
</div>
// AM/PM selector and action buttons
<div class="flex justify-between items-center">
if p.Use12Hours {
<div class="flex gap-1">
<button
type="button"
data-tui-timepicker-period="AM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.AMLabel }
</button>
<button
type="button"
data-tui-timepicker-period="PM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.PMLabel }
</button>
</div>
} else {
<div></div>
}
@button.Button(button.Props{
Type: "button",
Variant: button.VariantSecondary,
Size: button.SizeSm,
Attributes: templ.Attributes{
"data-tui-timepicker-done": "true",
},
}) {
Done
}
</div>
</div>
}
}
}
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/timepicker.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,153 @@
// templui component toast - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/toast
package toast
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgething/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgething/internal/utils"
"strconv"
)
type Variant string
type Position string
const (
VariantDefault Variant = "default"
VariantSuccess Variant = "success"
VariantError Variant = "error"
VariantWarning Variant = "warning"
VariantInfo Variant = "info"
)
const (
PositionTopRight Position = "top-right"
PositionTopLeft Position = "top-left"
PositionTopCenter Position = "top-center"
PositionBottomRight Position = "bottom-right"
PositionBottomLeft Position = "bottom-left"
PositionBottomCenter Position = "bottom-center"
)
type Props struct {
ID string
Class string
Attributes templ.Attributes
Title string
Description string
Variant Variant
Position Position
Duration int
Dismissible bool
ShowIndicator bool
Icon bool
}
templ Toast(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
// Set defaults
if p.Variant == "" {
{{ p.Variant = VariantDefault }}
}
if p.Position == "" {
{{ p.Position = PositionBottomRight }}
}
if p.Duration == 0 {
{{ p.Duration = 3000 }}
}
<div
id={ p.ID }
data-tui-toast
data-tui-toast-duration={ strconv.Itoa(p.Duration) }
data-position={ string(p.Position) }
data-variant={ string(p.Variant) }
class={ utils.TwMerge(
// Base styles
"z-50 fixed pointer-events-auto p-4 w-full md:max-w-[420px]",
// Animation
"animate-in fade-in slide-in-from-bottom-4 duration-300",
// Position-based styles using data attributes
"data-[position=top-right]:top-0 data-[position=top-right]:right-0",
"data-[position=top-left]:top-0 data-[position=top-left]:left-0",
"data-[position=top-center]:top-0 data-[position=top-center]:left-1/2 data-[position=top-center]:-translate-x-1/2",
"data-[position=bottom-right]:bottom-0 data-[position=bottom-right]:right-0",
"data-[position=bottom-left]:bottom-0 data-[position=bottom-left]:left-0",
"data-[position=bottom-center]:bottom-0 data-[position=bottom-center]:left-1/2 data-[position=bottom-center]:-translate-x-1/2",
// Slide direction based on position
"data-[position*=top]:slide-in-from-top-4",
"data-[position*=bottom]:slide-in-from-bottom-4",
p.Class,
) }
{ p.Attributes... }
>
<div class="w-full bg-popover text-popover-foreground rounded-lg shadow-xs border pt-5 pb-4 px-4 flex items-center justify-center relative overflow-hidden group">
// Progress indicator
if p.ShowIndicator && p.Duration > 0 {
<div class="absolute top-0 left-0 right-0 h-1 overflow-hidden">
<div
class={ utils.TwMerge(
"toast-progress h-full origin-left transition-transform ease-linear",
// Variant colors
"data-[variant=default]:bg-gray-500",
"data-[variant=success]:bg-green-500",
"data-[variant=error]:bg-red-500",
"data-[variant=warning]:bg-yellow-500",
"data-[variant=info]:bg-blue-500",
) }
data-variant={ string(p.Variant) }
data-duration={ strconv.Itoa(p.Duration) }
></div>
</div>
}
// Icon
if p.Icon {
switch p.Variant {
case VariantSuccess:
@icon.CircleCheck(icon.Props{Size: 22, Class: "text-green-500 mr-3 flex-shrink-0"})
case VariantError:
@icon.CircleX(icon.Props{Size: 22, Class: "text-red-500 mr-3 flex-shrink-0"})
case VariantWarning:
@icon.TriangleAlert(icon.Props{Size: 22, Class: "text-yellow-500 mr-3 flex-shrink-0"})
case VariantInfo:
@icon.Info(icon.Props{Size: 22, Class: "text-blue-500 mr-3 flex-shrink-0"})
}
}
// Content
<span class="flex-1 min-w-0">
if p.Title != "" {
<p class="text-sm font-semibold truncate">{ p.Title }</p>
}
if p.Description != "" {
<p class="text-sm opacity-90 mt-1">{ p.Description }</p>
}
</span>
// Dismiss button
if p.Dismissible {
@button.Button(button.Props{
Size: button.SizeIcon,
Variant: button.VariantGhost,
Attributes: templ.Attributes{
"aria-label": "Close",
"data-tui-toast-dismiss": "",
"type": "button",
},
}) {
@icon.X(icon.Props{
Size: 18,
Class: "opacity-75 hover:opacity-100",
})
}
}
</div>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ "/assets/js/toast.min.js?v=" + utils.ScriptVersion }></script>
}

View file

@ -0,0 +1,94 @@
// templui component tooltip - version: v0.101.0 installed by templui v0.101.0
// 📚 Documentation: https://templui.io/docs/components/tooltip
package tooltip
import (
"git.juancwu.dev/juancwu/budgething/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgething/internal/utils"
)
type Position string
const (
PositionTop Position = "top"
PositionRight Position = "right"
PositionBottom Position = "bottom"
PositionLeft Position = "left"
)
// Map tooltip positions to popover positions
func mapTooltipPositionToPopover(position Position) popover.Placement {
switch position {
case PositionTop:
return popover.PlacementTop
case PositionRight:
return popover.PlacementRight
case PositionBottom:
return popover.PlacementBottom
case PositionLeft:
return popover.PlacementLeft
default:
return popover.PlacementTop
}
}
type Props struct {
ID string
Class string
Attributes templ.Attributes
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
ShowArrow bool
Position Position
HoverDelay int
HoverOutDelay int
}
templ Tooltip(props ...Props) {
{ children... }
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
TriggerType: popover.TriggerTypeHover,
For: p.For,
}) {
{ children... }
}
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Content(popover.ContentProps{
ID: p.ID,
Class: utils.TwMerge("px-4 py-1 bg-foreground text-background [&_[data-tui-popover-arrow]]:!bg-foreground [&_[data-tui-popover-arrow]]:!border-0", p.Class),
Attributes: p.Attributes,
Placement: mapTooltipPositionToPopover(p.Position),
ShowArrow: p.ShowArrow,
HoverDelay: p.HoverDelay,
HoverOutDelay: p.HoverOutDelay,
}) {
{ children... }
}
}

60
internal/utils/templui.go Normal file
View file

@ -0,0 +1,60 @@
// templui util templui.go - version: v0.101.0 installed by templui v0.101.0
package utils
import (
"fmt"
"time"
"crypto/rand"
"github.com/a-h/templ"
twmerge "github.com/Oudwins/tailwind-merge-go"
)
// TwMerge combines Tailwind classes and resolves conflicts.
// Example: "bg-red-500 hover:bg-blue-500", "bg-green-500" → "hover:bg-blue-500 bg-green-500"
func TwMerge(classes ...string) string {
return twmerge.Merge(classes...)
}
// TwIf returns value if condition is true, otherwise an empty value of type T.
// Example: true, "bg-red-500" → "bg-red-500"
func If[T comparable](condition bool, value T) T {
var empty T
if condition {
return value
}
return empty
}
// TwIfElse returns trueValue if condition is true, otherwise falseValue.
// Example: true, "bg-red-500", "bg-gray-300" → "bg-red-500"
func IfElse[T any](condition bool, trueValue T, falseValue T) T {
if condition {
return trueValue
}
return falseValue
}
// MergeAttributes combines multiple Attributes into one.
// Example: MergeAttributes(attr1, attr2) → combined attributes
func MergeAttributes(attrs ...templ.Attributes) templ.Attributes {
merged := templ.Attributes{}
for _, attr := range attrs {
for k, v := range attr {
merged[k] = v
}
}
return merged
}
// RandomID generates a random ID string.
// Example: RandomID() → "id-1a2b3c"
func RandomID() string {
return fmt.Sprintf("id-%s", rand.Text())
}
// ScriptVersion is a timestamp generated at app start for cache busting.
// Used in Script() templates to append ?v=<timestamp> to script URLs.
var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())