commit ec3924678206fc34fe0505ab897639a846f2df80
Author: Silas Brack <silasbrack@gmail.com>
Date: Sun, 31 May 2026 22:22:38 +0200
feat: initial commit
Diffstat:
82 files changed, 8780 insertions(+), 0 deletions(-)
diff --git a/.formatter.exs b/.formatter.exs
@@ -0,0 +1,6 @@
+[
+ import_deps: [:ecto, :ecto_sql, :phoenix],
+ subdirectories: ["priv/*/migrations"],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
+]
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,41 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Temporary files, for example, from tests.
+/tmp/
+
+# Ignore package tarball (built via "mix hex.build").
+daily_tracker-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
+# Nix dev shell
+/.nix-mix/
+/.nix-hex/
+
diff --git a/AGENTS.md b/AGENTS.md
@@ -0,0 +1,510 @@
+This is a web application written using the Phoenix web framework.
+
+## Project guidelines
+
+- Use `mix precommit` alias when you are done with all changes and fix any pending issues
+- Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps
+
+### Phoenix v1.8 guidelines
+
+- **Always** begin your LiveView templates with `<Layouts.app flash={@flash} ...>` which wraps all inner content
+- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again
+- Anytime you run into errors with no `current_scope` assign:
+ - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `<Layouts.app>`
+ - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed
+- Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module
+- Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar
+- **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors
+- If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your
+custom classes must fully style the input
+
+### JS and CSS guidelines
+
+- **Use Tailwind CSS classes and custom CSS rules** to create polished, responsive, and visually stunning interfaces.
+- Tailwindcss v4 **no longer needs a tailwind.config.js** and uses a new import syntax in `app.css`:
+
+ @import "tailwindcss" source(none);
+ @source "../css";
+ @source "../js";
+ @source "../../lib/my_app_web";
+
+- **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new`
+- **Never** use `@apply` when writing raw css
+- **Always** manually write your own tailwind-based components instead of using daisyUI for a unique, world-class design
+- Out of the box **only the app.js and app.css bundles are supported**
+ - You cannot reference an external vendor'd script `src` or link `href` in the layouts
+ - You must import the vendor deps into app.js and app.css to use them
+ - **Never write inline <script>custom js</script> tags within templates**
+
+### UI/UX & design guidelines
+
+- **Produce world-class UI designs** with a focus on usability, aesthetics, and modern design principles
+- Implement **subtle micro-interactions** (e.g., button hover effects, and smooth transitions)
+- Ensure **clean typography, spacing, and layout balance** for a refined, premium look
+- Focus on **delightful details** like hover effects, loading states, and smooth page transitions
+
+
+<!-- phoenix-gen-auth-start -->
+## Authentication
+
+- **Always** handle authentication flow at the router level with proper redirects
+- **Always** be mindful of where to place routes. `phx.gen.auth` creates multiple router plugs and `live_session` scopes:
+ - A plug `:fetch_current_scope_for_user` that is included in the default browser pipeline
+ - A plug `:require_authenticated_user` that redirects to the log in page when the user is not authenticated
+ - A `live_session :current_user` scope - for routes that need the current user but don't require authentication, similar to `:fetch_current_scope_for_user`
+ - A `live_session :require_authenticated_user` scope - for routes that require authentication, similar to the plug with the same name
+ - In both cases, a `@current_scope` is assigned to the Plug connection and LiveView socket
+ - A plug `redirect_if_user_is_authenticated` that redirects to a default path in case the user is authenticated - useful for a registration page that should only be shown to unauthenticated users
+- **Always let the user know in which router scopes, `live_session`, and pipeline you are placing the route, AND SAY WHY**
+- `phx.gen.auth` assigns the `current_scope` assign - it **does not assign a `current_user` assign**
+- Always pass the assign `current_scope` to context modules as first argument. When performing queries, use `current_scope.user` to filter the query results
+- To derive/access `current_user` in templates, **always use the `@current_scope.user`**, never use **`@current_user`** in templates or LiveViews
+- **Never** duplicate `live_session` names. A `live_session :current_user` can only be defined __once__ in the router, so all routes for the `live_session :current_user` must be grouped in a single block
+- Anytime you hit `current_scope` errors or the logged in session isn't displaying the right content, **always double check the router and ensure you are using the correct plug and `live_session` as described below**
+
+### Routes that require authentication
+
+LiveViews that require login should **always be placed inside the __existing__ `live_session :require_authenticated_user` block**:
+
+ scope "/", AppWeb do
+ pipe_through [:browser, :require_authenticated_user]
+
+ live_session :require_authenticated_user,
+ on_mount: [{DailyTrackerWeb.UserAuth, :require_authenticated}] do
+ # phx.gen.auth generated routes
+ live "/users/settings", UserLive.Settings, :edit
+ live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
+ # our own routes that require logged in user
+ live "/", MyLiveThatRequiresAuth, :index
+ end
+ end
+
+Controller routes must be placed in a scope that sets the `:require_authenticated_user` plug:
+
+ scope "/", AppWeb do
+ pipe_through [:browser, :require_authenticated_user]
+
+ get "/", MyControllerThatRequiresAuth, :index
+ end
+
+### Routes that work with or without authentication
+
+LiveViews that can work with or without authentication, **always use the __existing__ `:current_user` scope**, ie:
+
+ scope "/", MyAppWeb do
+ pipe_through [:browser]
+
+ live_session :current_user,
+ on_mount: [{DailyTrackerWeb.UserAuth, :mount_current_scope}] do
+ # our own routes that work with or without authentication
+ live "/", PublicLive
+ end
+ end
+
+Controllers automatically have the `current_scope` available if they use the `:browser` pipeline.
+
+<!-- phoenix-gen-auth-end -->
+
+<!-- usage-rules-start -->
+
+<!-- phoenix:elixir-start -->
+## Elixir guidelines
+
+- Elixir lists **do not support index based access via the access syntax**
+
+ **Never do this (invalid)**:
+
+ i = 0
+ mylist = ["blue", "green"]
+ mylist[i]
+
+ Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
+
+ i = 0
+ mylist = ["blue", "green"]
+ Enum.at(mylist, i)
+
+- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
+ you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
+
+ # INVALID: we are rebinding inside the `if` and the result never gets assigned
+ if connected?(socket) do
+ socket = assign(socket, :val, val)
+ end
+
+ # VALID: we rebind the result of the `if` to a new variable
+ socket =
+ if connected?(socket) do
+ assign(socket, :val, val)
+ end
+
+- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
+- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
+- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
+- Don't use `String.to_atom/1` on user input (memory leak risk)
+- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
+- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
+- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
+
+## Mix guidelines
+
+- Read the docs and options before using tasks (by using `mix help task_name`)
+- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
+- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
+
+## Test guidelines
+
+- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests
+- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests
+ - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message:
+
+ ref = Process.monitor(pid)
+ assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
+
+ - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
+<!-- phoenix:elixir-end -->
+
+<!-- phoenix:phoenix-start -->
+## Phoenix guidelines
+
+- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
+
+- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
+
+ scope "/admin", AppWeb.Admin do
+ pipe_through :browser
+
+ live "/users", UserLive, :index
+ end
+
+ the UserLive route would point to the `AppWeb.Admin.UserLive` module
+
+- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
+<!-- phoenix:phoenix-end -->
+
+<!-- phoenix:ecto-start -->
+## Ecto Guidelines
+
+- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
+- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
+- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
+- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
+- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
+- Fields which are set programmatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
+- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
+<!-- phoenix:ecto-end -->
+
+<!-- phoenix:html-start -->
+## Phoenix HTML guidelines
+
+- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
+- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated
+- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]`
+- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`)
+- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name)
+
+- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals.
+
+ **Never do this (invalid)**:
+
+ <%= if condition do %>
+ ...
+ <% else if other_condition %>
+ ...
+ <% end %>
+
+ Instead **always** do this:
+
+ <%= cond do %>
+ <% condition -> %>
+ ...
+ <% condition2 -> %>
+ ...
+ <% true -> %>
+ ...
+ <% end %>
+
+- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `<pre>` or `<code>` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
+
+ <code phx-no-curly-interpolation>
+ let obj = {key: "val"}
+ </code>
+
+ Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
+
+- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
+
+ <a class={[
+ "px-2 text-white",
+ @some_flag && "py-5",
+ if(@other_condition, do: "border-red-500", else: "border-blue-100"),
+ ...
+ ]}>Text</a>
+
+ and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
+
+ and **never** do this, since it's invalid (note the missing `[` and `]`):
+
+ <a class={
+ "px-2 text-white",
+ @some_flag && "py-5"
+ }> ...
+ => Raises compile syntax error on invalid HEEx attr syntax
+
+- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
+- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
+- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
+
+ **Always** do this:
+
+ <div id={@id}>
+ {@my_assign}
+ <%= if @some_block_condition do %>
+ {@another_assign}
+ <% end %>
+ </div>
+
+ and **Never** do this – the program will terminate with a syntax error:
+
+ <%!-- THIS IS INVALID NEVER EVER DO THIS --%>
+ <div id="<%= @invalid_interpolation %>">
+ {if @invalid_block_construct do}
+ {end}
+ </div>
+<!-- phoenix:html-end -->
+
+<!-- phoenix:liveview-start -->
+## Phoenix LiveView guidelines
+
+- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
+- **Avoid LiveComponent's** unless you have a strong, specific need for them
+- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
+
+### LiveView streams
+
+- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
+ - basic append of N items - `stream(socket, :messages, [new_msg])`
+ - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
+ - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
+ - deleting items - `stream_delete(socket, :messages, msg)`
+
+- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
+
+ <div id="messages" phx-update="stream">
+ <div :for={{id, msg} <- @streams.messages} id={id}>
+ {msg.text}
+ </div>
+ </div>
+
+- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
+
+ def handle_event("filter", %{"filter" => filter}, socket) do
+ # re-fetch the messages based on the filter
+ messages = list_messages(filter)
+
+ {:noreply,
+ socket
+ |> assign(:messages_empty?, messages == [])
+ # reset the stream with the new messages
+ |> stream(:messages, messages, reset: true)}
+ end
+
+- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
+
+ <div id="tasks" phx-update="stream">
+ <div class="hidden only:block">No tasks yet</div>
+ <div :for={{id, task} <- @streams.tasks} id={id}>
+ {task.name}
+ </div>
+ </div>
+
+ The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
+
+- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items
+ along with the updated assign:
+
+ def handle_event("edit_message", %{"message_id" => message_id}, socket) do
+ message = Chat.get_message!(message_id)
+ edit_form = to_form(Chat.change_message(message, %{content: message.content}))
+
+ # re-insert message so @editing_message_id toggle logic takes effect for that stream item
+ {:noreply,
+ socket
+ |> stream_insert(:messages, message)
+ |> assign(:editing_message_id, String.to_integer(message_id))
+ |> assign(:edit_form, edit_form)}
+ end
+
+ And in the template:
+
+ <div id="messages" phx-update="stream">
+ <div :for={{id, message} <- @streams.messages} id={id} class="flex group">
+ {message.username}
+ <%= if @editing_message_id == message.id do %>
+ <%!-- Edit mode --%>
+ <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit">
+ ...
+ </.form>
+ <% end %>
+ </div>
+ </div>
+
+- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
+
+### LiveView JavaScript interop
+
+- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
+- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised
+
+LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx,
+and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor.
+
+#### Inline colocated js hooks
+
+**Never** write raw embedded `<script>` tags in heex as they are incompatible with LiveView.
+Instead, **always use a colocated js hook script tag (`:type={Phoenix.LiveView.ColocatedHook}`)
+when writing scripts inside the template**:
+
+ <input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" />
+ <script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
+ export default {
+ mounted() {
+ this.el.addEventListener("input", e => {
+ let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
+ if(match) {
+ this.el.value = `${match[1]}-${match[2]}-${match[3]}`
+ }
+ })
+ }
+ }
+ </script>
+
+- colocated hooks are automatically integrated into the app.js bundle
+- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber`
+
+#### External phx-hook
+
+External JS hooks (`<div id="myhook" phx-hook="MyHook">`) must be placed in `assets/js/` and passed to the
+LiveSocket constructor:
+
+ const MyHook = {
+ mounted() { ... }
+ }
+ let liveSocket = new LiveSocket("/live", Socket, {
+ hooks: { MyHook }
+ });
+
+#### Pushing events between client and server
+
+Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle.
+**Always** return or rebind the socket on `push_event/3` when pushing events:
+
+ # re-bind socket so we maintain event state to be pushed
+ socket = push_event(socket, "my_event", %{...})
+
+ # or return the modified socket directly:
+ def handle_event("some_event", _, socket) do
+ {:noreply, push_event(socket, "my_event", %{...})}
+ end
+
+Pushed events can then be picked up in a JS hook with `this.handleEvent`:
+
+ mounted() {
+ this.handleEvent("my_event", data => console.log("from server:", data));
+ }
+
+Clients can also push an event to the server and receive a reply with `this.pushEvent`:
+
+ mounted() {
+ this.el.addEventListener("click", e => {
+ this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply));
+ })
+ }
+
+Where the server handled it via:
+
+ def handle_event("my_event", %{"one" => 1}, socket) do
+ {:reply, %{two: 2}, socket}
+ end
+
+### LiveView tests
+
+- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
+- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
+- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
+- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
+- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
+- Instead of relying on testing text content, which can change, favor testing for the presence of key elements
+- Focus on testing outcomes rather than implementation details
+- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
+- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
+
+ html = render(view)
+ document = LazyHTML.from_fragment(html)
+ matches = LazyHTML.filter(document, "your-complex-selector")
+ IO.inspect(matches, label: "Matches")
+
+### Form handling
+
+#### Creating a form from params
+
+If you want to create a form based on `handle_event` params:
+
+ def handle_event("submitted", params, socket) do
+ {:noreply, assign(socket, form: to_form(params))}
+ end
+
+When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
+
+You can also specify a name to nest the params:
+
+ def handle_event("submitted", %{"user" => user_params}, socket) do
+ {:noreply, assign(socket, form: to_form(user_params, as: :user))}
+ end
+
+#### Creating a form from changesets
+
+When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema:
+
+ defmodule MyApp.Users.User do
+ use Ecto.Schema
+ ...
+ end
+
+And then you create a changeset that you pass to `to_form`:
+
+ %MyApp.Users.User{}
+ |> Ecto.Changeset.change()
+ |> to_form()
+
+Once the form is submitted, the params will be available under `%{"user" => user_params}`.
+
+In the template, the form form assign can be passed to the `<.form>` function component:
+
+ <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
+ <.input field={@form[:field]} type="text" />
+ </.form>
+
+Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
+
+#### Avoiding form errors
+
+**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**:
+
+ <%!-- ALWAYS do this (valid) --%>
+ <.form for={@form} id="my-form">
+ <.input field={@form[:field]} type="text" />
+ </.form>
+
+And **never** do this:
+
+ <%!-- NEVER do this (invalid) --%>
+ <.form for={@changeset} id="my-form">
+ <.input field={@changeset[:field]} type="text" />
+ </.form>
+
+- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
+- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
+<!-- phoenix:liveview-end -->
+
+<!-- usage-rules-end -->
+\ No newline at end of file
diff --git a/README.md b/README.md
@@ -0,0 +1,18 @@
+# DailyTracker
+
+To start your Phoenix server:
+
+* Run `mix setup` to install and setup dependencies
+* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+* Official website: https://www.phoenixframework.org/
+* Guides: https://hexdocs.pm/phoenix/overview.html
+* Docs: https://hexdocs.pm/phoenix
+* Forum: https://elixirforum.com/c/phoenix-forum
+* Source: https://github.com/phoenixframework/phoenix
diff --git a/assets/css/app.css b/assets/css/app.css
@@ -0,0 +1,105 @@
+/* See the Tailwind configuration guide for advanced usage
+ https://tailwindcss.com/docs/configuration */
+
+@import "tailwindcss" source(none);
+@source "../css";
+@source "../js";
+@source "../../lib/daily_tracker_web";
+
+/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
+ The heroicons installation itself is managed by your mix.exs */
+@plugin "../vendor/heroicons";
+
+/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
+ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
+ Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
+@plugin "../vendor/daisyui" {
+ themes: false;
+}
+
+/* daisyUI theme plugin. You can update this file by fetching the latest version with:
+ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
+ We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
+ on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
+@plugin "../vendor/daisyui-theme" {
+ name: "dark";
+ default: false;
+ prefersdark: true;
+ color-scheme: "dark";
+ --color-base-100: oklch(30.33% 0.016 252.42);
+ --color-base-200: oklch(25.26% 0.014 253.1);
+ --color-base-300: oklch(20.15% 0.012 254.09);
+ --color-base-content: oklch(97.807% 0.029 256.847);
+ --color-primary: oklch(58% 0.233 277.117);
+ --color-primary-content: oklch(96% 0.018 272.314);
+ --color-secondary: oklch(58% 0.233 277.117);
+ --color-secondary-content: oklch(96% 0.018 272.314);
+ --color-accent: oklch(60% 0.25 292.717);
+ --color-accent-content: oklch(96% 0.016 293.756);
+ --color-neutral: oklch(37% 0.044 257.287);
+ --color-neutral-content: oklch(98% 0.003 247.858);
+ --color-info: oklch(58% 0.158 241.966);
+ --color-info-content: oklch(97% 0.013 236.62);
+ --color-success: oklch(60% 0.118 184.704);
+ --color-success-content: oklch(98% 0.014 180.72);
+ --color-warning: oklch(66% 0.179 58.318);
+ --color-warning-content: oklch(98% 0.022 95.277);
+ --color-error: oklch(58% 0.253 17.585);
+ --color-error-content: oklch(96% 0.015 12.422);
+ --radius-selector: 0.25rem;
+ --radius-field: 0.25rem;
+ --radius-box: 0.5rem;
+ --size-selector: 0.21875rem;
+ --size-field: 0.21875rem;
+ --border: 1.5px;
+ --depth: 1;
+ --noise: 0;
+}
+
+@plugin "../vendor/daisyui-theme" {
+ name: "light";
+ default: true;
+ prefersdark: false;
+ color-scheme: "light";
+ --color-base-100: oklch(98% 0 0);
+ --color-base-200: oklch(96% 0.001 286.375);
+ --color-base-300: oklch(92% 0.004 286.32);
+ --color-base-content: oklch(21% 0.006 285.885);
+ --color-primary: oklch(70% 0.213 47.604);
+ --color-primary-content: oklch(98% 0.016 73.684);
+ --color-secondary: oklch(55% 0.027 264.364);
+ --color-secondary-content: oklch(98% 0.002 247.839);
+ --color-accent: oklch(0% 0 0);
+ --color-accent-content: oklch(100% 0 0);
+ --color-neutral: oklch(44% 0.017 285.786);
+ --color-neutral-content: oklch(98% 0 0);
+ --color-info: oklch(62% 0.214 259.815);
+ --color-info-content: oklch(97% 0.014 254.604);
+ --color-success: oklch(70% 0.14 182.503);
+ --color-success-content: oklch(98% 0.014 180.72);
+ --color-warning: oklch(66% 0.179 58.318);
+ --color-warning-content: oklch(98% 0.022 95.277);
+ --color-error: oklch(58% 0.253 17.585);
+ --color-error-content: oklch(96% 0.015 12.422);
+ --radius-selector: 0.25rem;
+ --radius-field: 0.25rem;
+ --radius-box: 0.5rem;
+ --size-selector: 0.21875rem;
+ --size-field: 0.21875rem;
+ --border: 1.5px;
+ --depth: 1;
+ --noise: 0;
+}
+
+/* Add variants based on LiveView classes */
+@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
+@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
+@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
+
+/* Use the data attribute for dark mode */
+@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
+
+/* Make LiveView wrapper divs transparent for layout */
+[data-phx-session], [data-phx-teleported-src] { display: contents }
+
+/* This file is for your main application CSS */
diff --git a/assets/js/app.js b/assets/js/app.js
@@ -0,0 +1,83 @@
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
+// To load it, simply add a second `<link>` to your `root.html.heex` file.
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+import {Socket} from "phoenix"
+import {LiveSocket} from "phoenix_live_view"
+import {hooks as colocatedHooks} from "phoenix-colocated/daily_tracker"
+import topbar from "../vendor/topbar"
+
+const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+const liveSocket = new LiveSocket("/live", Socket, {
+ longPollFallbackMs: 2500,
+ params: {_csrf_token: csrfToken},
+ hooks: {...colocatedHooks},
+})
+
+// Show progress bar on live navigation and form submits
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
+window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
+
+// connect if there are any LiveViews on the page
+liveSocket.connect()
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket
+
+// The lines below enable quality of life phoenix_live_reload
+// development features:
+//
+// 1. stream server logs to the browser console
+// 2. click on elements to jump to their definitions in your code editor
+//
+if (process.env.NODE_ENV === "development") {
+ window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
+ // Enable server log streaming to client.
+ // Disable with reloader.disableServerLogs()
+ reloader.enableServerLogs()
+
+ // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
+ //
+ // * click with "c" key pressed to open at caller location
+ // * click with "d" key pressed to open at function component definition location
+ let keyDown
+ window.addEventListener("keydown", e => keyDown = e.key)
+ window.addEventListener("keyup", _e => keyDown = null)
+ window.addEventListener("click", e => {
+ if(keyDown === "c"){
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ reloader.openEditorAtCaller(e.target)
+ } else if(keyDown === "d"){
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ reloader.openEditorAtDef(e.target)
+ }
+ }, true)
+
+ window.liveReloader = reloader
+ })
+}
+
diff --git a/assets/tsconfig.json b/assets/tsconfig.json
@@ -0,0 +1,32 @@
+// This file is needed on most editors to enable the intelligent autocompletion
+// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
+//
+// Note: This file assumes a basic esbuild setup without node_modules.
+// We include a generic paths alias to deps to mimic how esbuild resolves
+// the Phoenix and LiveView JavaScript assets.
+// If you have a package.json in your project, you should remove the
+// paths configuration and instead add the phoenix dependencies to the
+// dependencies section of your package.json:
+//
+// {
+// ...
+// "dependencies": {
+// ...,
+// "phoenix": "../deps/phoenix",
+// "phoenix_html": "../deps/phoenix_html",
+// "phoenix_live_view": "../deps/phoenix_live_view"
+// }
+// }
+//
+// Feel free to adjust this configuration however you need.
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "*": ["../deps/*"]
+ },
+ "allowJs": true,
+ "noEmit": true
+ },
+ "include": ["js/**/*"]
+}
diff --git a/assets/vendor/daisyui-theme.js b/assets/vendor/daisyui-theme.js
@@ -0,0 +1,124 @@
+/** 🌼
+ * @license MIT
+ * daisyUI bundle
+ * https://daisyui.com/
+ */
+
+var __defProp = Object.defineProperty;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __moduleCache = /* @__PURE__ */ new WeakMap;
+var __toCommonJS = (from) => {
+ var entry = __moduleCache.get(from), desc;
+ if (entry)
+ return entry;
+ entry = __defProp({}, "__esModule", { value: true });
+ if (from && typeof from === "object" || typeof from === "function")
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
+ get: () => from[key],
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
+ }));
+ __moduleCache.set(from, entry);
+ return entry;
+};
+var __export = (target, all) => {
+ for (var name in all)
+ __defProp(target, name, {
+ get: all[name],
+ enumerable: true,
+ configurable: true,
+ set: (newValue) => all[name] = () => newValue
+ });
+};
+
+// packages/daisyui/theme/index.js
+var exports_theme = {};
+__export(exports_theme, {
+ default: () => theme_default
+});
+module.exports = __toCommonJS(exports_theme);
+
+// packages/daisyui/functions/plugin.js
+var plugin = {
+ withOptions: (pluginFunction, configFunction = () => ({})) => {
+ const optionsFunction = (options) => {
+ const handler = pluginFunction(options);
+ const config = configFunction(options);
+ return { handler, config };
+ };
+ optionsFunction.__isOptionsFunction = true;
+ return optionsFunction;
+ }
+};
+
+// packages/daisyui/theme/object.js
+var object_default = { cyberpunk: { "color-scheme": "light", "--color-base-100": "oklch(94.51% 0.179 104.32)", "--color-base-200": "oklch(91.51% 0.179 104.32)", "--color-base-300": "oklch(85.51% 0.179 104.32)", "--color-base-content": "oklch(0% 0 0)", "--color-primary": "oklch(74.22% 0.209 6.35)", "--color-primary-content": "oklch(14.844% 0.041 6.35)", "--color-secondary": "oklch(83.33% 0.184 204.72)", "--color-secondary-content": "oklch(16.666% 0.036 204.72)", "--color-accent": "oklch(71.86% 0.217 310.43)", "--color-accent-content": "oklch(14.372% 0.043 310.43)", "--color-neutral": "oklch(23.04% 0.065 269.31)", "--color-neutral-content": "oklch(94.51% 0.179 104.32)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "0rem", "--radius-field": "0rem", "--radius-box": "0rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, acid: { "color-scheme": "light", "--color-base-100": "oklch(98% 0 0)", "--color-base-200": "oklch(95% 0 0)", "--color-base-300": "oklch(91% 0 0)", "--color-base-content": "oklch(0% 0 0)", "--color-primary": "oklch(71.9% 0.357 330.759)", "--color-primary-content": "oklch(14.38% 0.071 330.759)", "--color-secondary": "oklch(73.37% 0.224 48.25)", "--color-secondary-content": "oklch(14.674% 0.044 48.25)", "--color-accent": "oklch(92.78% 0.264 122.962)", "--color-accent-content": "oklch(18.556% 0.052 122.962)", "--color-neutral": "oklch(21.31% 0.128 278.68)", "--color-neutral-content": "oklch(84.262% 0.025 278.68)", "--color-info": "oklch(60.72% 0.227 252.05)", "--color-info-content": "oklch(12.144% 0.045 252.05)", "--color-success": "oklch(85.72% 0.266 158.53)", "--color-success-content": "oklch(17.144% 0.053 158.53)", "--color-warning": "oklch(91.01% 0.212 100.5)", "--color-warning-content": "oklch(18.202% 0.042 100.5)", "--color-error": "oklch(64.84% 0.293 29.349)", "--color-error-content": "oklch(12.968% 0.058 29.349)", "--radius-selector": "1rem", "--radius-field": "1rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, black: { "color-scheme": "dark", "--color-base-100": "oklch(0% 0 0)", "--color-base-200": "oklch(19% 0 0)", "--color-base-300": "oklch(22% 0 0)", "--color-base-content": "oklch(87.609% 0 0)", "--color-primary": "oklch(35% 0 0)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(35% 0 0)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(35% 0 0)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(35% 0 0)", "--color-neutral-content": "oklch(100% 0 0)", "--color-info": "oklch(45.201% 0.313 264.052)", "--color-info-content": "oklch(89.04% 0.062 264.052)", "--color-success": "oklch(51.975% 0.176 142.495)", "--color-success-content": "oklch(90.395% 0.035 142.495)", "--color-warning": "oklch(96.798% 0.211 109.769)", "--color-warning-content": "oklch(19.359% 0.042 109.769)", "--color-error": "oklch(62.795% 0.257 29.233)", "--color-error-content": "oklch(12.559% 0.051 29.233)", "--radius-selector": "0rem", "--radius-field": "0rem", "--radius-box": "0rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, dark: { "color-scheme": "dark", "--color-base-100": "oklch(25.33% 0.016 252.42)", "--color-base-200": "oklch(23.26% 0.014 253.1)", "--color-base-300": "oklch(21.15% 0.012 254.09)", "--color-base-content": "oklch(97.807% 0.029 256.847)", "--color-primary": "oklch(58% 0.233 277.117)", "--color-primary-content": "oklch(96% 0.018 272.314)", "--color-secondary": "oklch(65% 0.241 354.308)", "--color-secondary-content": "oklch(94% 0.028 342.258)", "--color-accent": "oklch(77% 0.152 181.912)", "--color-accent-content": "oklch(38% 0.063 188.416)", "--color-neutral": "oklch(14% 0.005 285.823)", "--color-neutral-content": "oklch(92% 0.004 286.32)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(76% 0.177 163.223)", "--color-success-content": "oklch(37% 0.077 168.94)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(71% 0.194 13.428)", "--color-error-content": "oklch(27% 0.105 12.094)", "--radius-selector": "0.5rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, light: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(98% 0 0)", "--color-base-300": "oklch(95% 0 0)", "--color-base-content": "oklch(21% 0.006 285.885)", "--color-primary": "oklch(45% 0.24 277.023)", "--color-primary-content": "oklch(93% 0.034 272.788)", "--color-secondary": "oklch(65% 0.241 354.308)", "--color-secondary-content": "oklch(94% 0.028 342.258)", "--color-accent": "oklch(77% 0.152 181.912)", "--color-accent-content": "oklch(38% 0.063 188.416)", "--color-neutral": "oklch(14% 0.005 285.823)", "--color-neutral-content": "oklch(92% 0.004 286.32)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(76% 0.177 163.223)", "--color-success-content": "oklch(37% 0.077 168.94)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(71% 0.194 13.428)", "--color-error-content": "oklch(27% 0.105 12.094)", "--radius-selector": "0.5rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, luxury: { "color-scheme": "dark", "--color-base-100": "oklch(14.076% 0.004 285.822)", "--color-base-200": "oklch(20.219% 0.004 308.229)", "--color-base-300": "oklch(23.219% 0.004 308.229)", "--color-base-content": "oklch(75.687% 0.123 76.89)", "--color-primary": "oklch(100% 0 0)", "--color-primary-content": "oklch(20% 0 0)", "--color-secondary": "oklch(27.581% 0.064 261.069)", "--color-secondary-content": "oklch(85.516% 0.012 261.069)", "--color-accent": "oklch(36.674% 0.051 338.825)", "--color-accent-content": "oklch(87.334% 0.01 338.825)", "--color-neutral": "oklch(24.27% 0.057 59.825)", "--color-neutral-content": "oklch(93.203% 0.089 90.861)", "--color-info": "oklch(79.061% 0.121 237.133)", "--color-info-content": "oklch(15.812% 0.024 237.133)", "--color-success": "oklch(78.119% 0.192 132.154)", "--color-success-content": "oklch(15.623% 0.038 132.154)", "--color-warning": "oklch(86.127% 0.136 102.891)", "--color-warning-content": "oklch(17.225% 0.027 102.891)", "--color-error": "oklch(71.753% 0.176 22.568)", "--color-error-content": "oklch(14.35% 0.035 22.568)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, dracula: { "color-scheme": "dark", "--color-base-100": "oklch(28.822% 0.022 277.508)", "--color-base-200": "oklch(26.805% 0.02 277.508)", "--color-base-300": "oklch(24.787% 0.019 277.508)", "--color-base-content": "oklch(97.747% 0.007 106.545)", "--color-primary": "oklch(75.461% 0.183 346.812)", "--color-primary-content": "oklch(15.092% 0.036 346.812)", "--color-secondary": "oklch(74.202% 0.148 301.883)", "--color-secondary-content": "oklch(14.84% 0.029 301.883)", "--color-accent": "oklch(83.392% 0.124 66.558)", "--color-accent-content": "oklch(16.678% 0.024 66.558)", "--color-neutral": "oklch(39.445% 0.032 275.524)", "--color-neutral-content": "oklch(87.889% 0.006 275.524)", "--color-info": "oklch(88.263% 0.093 212.846)", "--color-info-content": "oklch(17.652% 0.018 212.846)", "--color-success": "oklch(87.099% 0.219 148.024)", "--color-success-content": "oklch(17.419% 0.043 148.024)", "--color-warning": "oklch(95.533% 0.134 112.757)", "--color-warning-content": "oklch(19.106% 0.026 112.757)", "--color-error": "oklch(68.22% 0.206 24.43)", "--color-error-content": "oklch(13.644% 0.041 24.43)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, retro: { "color-scheme": "light", "--color-base-100": "oklch(91.637% 0.034 90.515)", "--color-base-200": "oklch(88.272% 0.049 91.774)", "--color-base-300": "oklch(84.133% 0.065 90.856)", "--color-base-content": "oklch(41% 0.112 45.904)", "--color-primary": "oklch(80% 0.114 19.571)", "--color-primary-content": "oklch(39% 0.141 25.723)", "--color-secondary": "oklch(92% 0.084 155.995)", "--color-secondary-content": "oklch(44% 0.119 151.328)", "--color-accent": "oklch(68% 0.162 75.834)", "--color-accent-content": "oklch(41% 0.112 45.904)", "--color-neutral": "oklch(44% 0.011 73.639)", "--color-neutral-content": "oklch(86% 0.005 56.366)", "--color-info": "oklch(58% 0.158 241.966)", "--color-info-content": "oklch(96% 0.059 95.617)", "--color-success": "oklch(51% 0.096 186.391)", "--color-success-content": "oklch(96% 0.059 95.617)", "--color-warning": "oklch(64% 0.222 41.116)", "--color-warning-content": "oklch(96% 0.059 95.617)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(40% 0.123 38.172)", "--radius-selector": "0.25rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, lofi: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97% 0 0)", "--color-base-300": "oklch(94% 0 0)", "--color-base-content": "oklch(0% 0 0)", "--color-primary": "oklch(15.906% 0 0)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(21.455% 0.001 17.278)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(26.861% 0 0)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(0% 0 0)", "--color-neutral-content": "oklch(100% 0 0)", "--color-info": "oklch(79.54% 0.103 205.9)", "--color-info-content": "oklch(15.908% 0.02 205.9)", "--color-success": "oklch(90.13% 0.153 164.14)", "--color-success-content": "oklch(18.026% 0.03 164.14)", "--color-warning": "oklch(88.37% 0.135 79.94)", "--color-warning-content": "oklch(17.674% 0.027 79.94)", "--color-error": "oklch(78.66% 0.15 28.47)", "--color-error-content": "oklch(15.732% 0.03 28.47)", "--radius-selector": "2rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, valentine: { "color-scheme": "light", "--color-base-100": "oklch(97% 0.014 343.198)", "--color-base-200": "oklch(94% 0.028 342.258)", "--color-base-300": "oklch(89% 0.061 343.231)", "--color-base-content": "oklch(52% 0.223 3.958)", "--color-primary": "oklch(65% 0.241 354.308)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(62% 0.265 303.9)", "--color-secondary-content": "oklch(97% 0.014 308.299)", "--color-accent": "oklch(82% 0.111 230.318)", "--color-accent-content": "oklch(39% 0.09 240.876)", "--color-neutral": "oklch(40% 0.153 2.432)", "--color-neutral-content": "oklch(89% 0.061 343.231)", "--color-info": "oklch(86% 0.127 207.078)", "--color-info-content": "oklch(44% 0.11 240.79)", "--color-success": "oklch(84% 0.143 164.978)", "--color-success-content": "oklch(43% 0.095 166.913)", "--color-warning": "oklch(75% 0.183 55.934)", "--color-warning-content": "oklch(26% 0.079 36.259)", "--color-error": "oklch(63% 0.237 25.331)", "--color-error-content": "oklch(97% 0.013 17.38)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, nord: { "color-scheme": "light", "--color-base-100": "oklch(95.127% 0.007 260.731)", "--color-base-200": "oklch(93.299% 0.01 261.788)", "--color-base-300": "oklch(89.925% 0.016 262.749)", "--color-base-content": "oklch(32.437% 0.022 264.182)", "--color-primary": "oklch(59.435% 0.077 254.027)", "--color-primary-content": "oklch(11.887% 0.015 254.027)", "--color-secondary": "oklch(69.651% 0.059 248.687)", "--color-secondary-content": "oklch(13.93% 0.011 248.687)", "--color-accent": "oklch(77.464% 0.062 217.469)", "--color-accent-content": "oklch(15.492% 0.012 217.469)", "--color-neutral": "oklch(45.229% 0.035 264.131)", "--color-neutral-content": "oklch(89.925% 0.016 262.749)", "--color-info": "oklch(69.207% 0.062 332.664)", "--color-info-content": "oklch(13.841% 0.012 332.664)", "--color-success": "oklch(76.827% 0.074 131.063)", "--color-success-content": "oklch(15.365% 0.014 131.063)", "--color-warning": "oklch(85.486% 0.089 84.093)", "--color-warning-content": "oklch(17.097% 0.017 84.093)", "--color-error": "oklch(60.61% 0.12 15.341)", "--color-error-content": "oklch(12.122% 0.024 15.341)", "--radius-selector": "1rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, lemonade: { "color-scheme": "light", "--color-base-100": "oklch(98.71% 0.02 123.72)", "--color-base-200": "oklch(91.8% 0.018 123.72)", "--color-base-300": "oklch(84.89% 0.017 123.72)", "--color-base-content": "oklch(19.742% 0.004 123.72)", "--color-primary": "oklch(58.92% 0.199 134.6)", "--color-primary-content": "oklch(11.784% 0.039 134.6)", "--color-secondary": "oklch(77.75% 0.196 111.09)", "--color-secondary-content": "oklch(15.55% 0.039 111.09)", "--color-accent": "oklch(85.39% 0.201 100.73)", "--color-accent-content": "oklch(17.078% 0.04 100.73)", "--color-neutral": "oklch(30.98% 0.075 108.6)", "--color-neutral-content": "oklch(86.196% 0.015 108.6)", "--color-info": "oklch(86.19% 0.047 224.14)", "--color-info-content": "oklch(17.238% 0.009 224.14)", "--color-success": "oklch(86.19% 0.047 157.85)", "--color-success-content": "oklch(17.238% 0.009 157.85)", "--color-warning": "oklch(86.19% 0.047 102.15)", "--color-warning-content": "oklch(17.238% 0.009 102.15)", "--color-error": "oklch(86.19% 0.047 25.85)", "--color-error-content": "oklch(17.238% 0.009 25.85)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, garden: { "color-scheme": "light", "--color-base-100": "oklch(92.951% 0.002 17.197)", "--color-base-200": "oklch(86.445% 0.002 17.197)", "--color-base-300": "oklch(79.938% 0.001 17.197)", "--color-base-content": "oklch(16.961% 0.001 17.32)", "--color-primary": "oklch(62.45% 0.278 3.836)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(48.495% 0.11 355.095)", "--color-secondary-content": "oklch(89.699% 0.022 355.095)", "--color-accent": "oklch(56.273% 0.054 154.39)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(24.155% 0.049 89.07)", "--color-neutral-content": "oklch(92.951% 0.002 17.197)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, aqua: { "color-scheme": "dark", "--color-base-100": "oklch(37% 0.146 265.522)", "--color-base-200": "oklch(28% 0.091 267.935)", "--color-base-300": "oklch(22% 0.091 267.935)", "--color-base-content": "oklch(90% 0.058 230.902)", "--color-primary": "oklch(85.661% 0.144 198.645)", "--color-primary-content": "oklch(40.124% 0.068 197.603)", "--color-secondary": "oklch(60.682% 0.108 309.782)", "--color-secondary-content": "oklch(96% 0.016 293.756)", "--color-accent": "oklch(93.426% 0.102 94.555)", "--color-accent-content": "oklch(18.685% 0.02 94.555)", "--color-neutral": "oklch(27% 0.146 265.522)", "--color-neutral-content": "oklch(80% 0.146 265.522)", "--color-info": "oklch(54.615% 0.215 262.88)", "--color-info-content": "oklch(90.923% 0.043 262.88)", "--color-success": "oklch(62.705% 0.169 149.213)", "--color-success-content": "oklch(12.541% 0.033 149.213)", "--color-warning": "oklch(66.584% 0.157 58.318)", "--color-warning-content": "oklch(27% 0.077 45.635)", "--color-error": "oklch(73.95% 0.19 27.33)", "--color-error-content": "oklch(14.79% 0.038 27.33)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, corporate: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(93% 0 0)", "--color-base-300": "oklch(86% 0 0)", "--color-base-content": "oklch(22.389% 0.031 278.072)", "--color-primary": "oklch(58% 0.158 241.966)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(55% 0.046 257.417)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(60% 0.118 184.704)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(0% 0 0)", "--color-neutral-content": "oklch(100% 0 0)", "--color-info": "oklch(60% 0.126 221.723)", "--color-info-content": "oklch(100% 0 0)", "--color-success": "oklch(62% 0.194 149.214)", "--color-success-content": "oklch(100% 0 0)", "--color-warning": "oklch(85% 0.199 91.936)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "0.25rem", "--radius-field": "0.25rem", "--radius-box": "0.25rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, pastel: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(98.462% 0.001 247.838)", "--color-base-300": "oklch(92.462% 0.001 247.838)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(90% 0.063 306.703)", "--color-primary-content": "oklch(49% 0.265 301.924)", "--color-secondary": "oklch(89% 0.058 10.001)", "--color-secondary-content": "oklch(51% 0.222 16.935)", "--color-accent": "oklch(90% 0.093 164.15)", "--color-accent-content": "oklch(50% 0.118 165.612)", "--color-neutral": "oklch(55% 0.046 257.417)", "--color-neutral-content": "oklch(92% 0.013 255.508)", "--color-info": "oklch(86% 0.127 207.078)", "--color-info-content": "oklch(52% 0.105 223.128)", "--color-success": "oklch(87% 0.15 154.449)", "--color-success-content": "oklch(52% 0.154 150.069)", "--color-warning": "oklch(83% 0.128 66.29)", "--color-warning-content": "oklch(55% 0.195 38.402)", "--color-error": "oklch(80% 0.114 19.571)", "--color-error-content": "oklch(50% 0.213 27.518)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "0", "--noise": "0" }, bumblebee: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97% 0 0)", "--color-base-300": "oklch(92% 0 0)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(85% 0.199 91.936)", "--color-primary-content": "oklch(42% 0.095 57.708)", "--color-secondary": "oklch(75% 0.183 55.934)", "--color-secondary-content": "oklch(40% 0.123 38.172)", "--color-accent": "oklch(0% 0 0)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(37% 0.01 67.558)", "--color-neutral-content": "oklch(92% 0.003 48.717)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(39% 0.09 240.876)", "--color-success": "oklch(76% 0.177 163.223)", "--color-success-content": "oklch(37% 0.077 168.94)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(39% 0.141 25.723)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, coffee: { "color-scheme": "dark", "--color-base-100": "oklch(24% 0.023 329.708)", "--color-base-200": "oklch(21% 0.021 329.708)", "--color-base-300": "oklch(16% 0.019 329.708)", "--color-base-content": "oklch(72.354% 0.092 79.129)", "--color-primary": "oklch(71.996% 0.123 62.756)", "--color-primary-content": "oklch(14.399% 0.024 62.756)", "--color-secondary": "oklch(34.465% 0.029 199.194)", "--color-secondary-content": "oklch(86.893% 0.005 199.194)", "--color-accent": "oklch(42.621% 0.074 224.389)", "--color-accent-content": "oklch(88.524% 0.014 224.389)", "--color-neutral": "oklch(16.51% 0.015 326.261)", "--color-neutral-content": "oklch(83.302% 0.003 326.261)", "--color-info": "oklch(79.49% 0.063 184.558)", "--color-info-content": "oklch(15.898% 0.012 184.558)", "--color-success": "oklch(74.722% 0.072 131.116)", "--color-success-content": "oklch(14.944% 0.014 131.116)", "--color-warning": "oklch(88.15% 0.14 87.722)", "--color-warning-content": "oklch(17.63% 0.028 87.722)", "--color-error": "oklch(77.318% 0.128 31.871)", "--color-error-content": "oklch(15.463% 0.025 31.871)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, silk: { "color-scheme": "light", "--color-base-100": "oklch(97% 0.0035 67.78)", "--color-base-200": "oklch(95% 0.0081 61.42)", "--color-base-300": "oklch(90% 0.0081 61.42)", "--color-base-content": "oklch(40% 0.0081 61.42)", "--color-primary": "oklch(23.27% 0.0249 284.3)", "--color-primary-content": "oklch(94.22% 0.2505 117.44)", "--color-secondary": "oklch(23.27% 0.0249 284.3)", "--color-secondary-content": "oklch(73.92% 0.2135 50.94)", "--color-accent": "oklch(23.27% 0.0249 284.3)", "--color-accent-content": "oklch(88.92% 0.2061 189.9)", "--color-neutral": "oklch(20% 0 0)", "--color-neutral-content": "oklch(80% 0.0081 61.42)", "--color-info": "oklch(80.39% 0.1148 241.68)", "--color-info-content": "oklch(30.39% 0.1148 241.68)", "--color-success": "oklch(83.92% 0.0901 136.87)", "--color-success-content": "oklch(23.92% 0.0901 136.87)", "--color-warning": "oklch(83.92% 0.1085 80)", "--color-warning-content": "oklch(43.92% 0.1085 80)", "--color-error": "oklch(75.1% 0.1814 22.37)", "--color-error-content": "oklch(35.1% 0.1814 22.37)", "--radius-selector": "2rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "1", "--noise": "0" }, sunset: { "color-scheme": "dark", "--color-base-100": "oklch(22% 0.019 237.69)", "--color-base-200": "oklch(20% 0.019 237.69)", "--color-base-300": "oklch(18% 0.019 237.69)", "--color-base-content": "oklch(77.383% 0.043 245.096)", "--color-primary": "oklch(74.703% 0.158 39.947)", "--color-primary-content": "oklch(14.94% 0.031 39.947)", "--color-secondary": "oklch(72.537% 0.177 2.72)", "--color-secondary-content": "oklch(14.507% 0.035 2.72)", "--color-accent": "oklch(71.294% 0.166 299.844)", "--color-accent-content": "oklch(14.258% 0.033 299.844)", "--color-neutral": "oklch(26% 0.019 237.69)", "--color-neutral-content": "oklch(70% 0.019 237.69)", "--color-info": "oklch(85.559% 0.085 206.015)", "--color-info-content": "oklch(17.111% 0.017 206.015)", "--color-success": "oklch(85.56% 0.085 144.778)", "--color-success-content": "oklch(17.112% 0.017 144.778)", "--color-warning": "oklch(85.569% 0.084 74.427)", "--color-warning-content": "oklch(17.113% 0.016 74.427)", "--color-error": "oklch(85.511% 0.078 16.886)", "--color-error-content": "oklch(17.102% 0.015 16.886)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, synthwave: { "color-scheme": "dark", "--color-base-100": "oklch(15% 0.09 281.288)", "--color-base-200": "oklch(20% 0.09 281.288)", "--color-base-300": "oklch(25% 0.09 281.288)", "--color-base-content": "oklch(78% 0.115 274.713)", "--color-primary": "oklch(71% 0.202 349.761)", "--color-primary-content": "oklch(28% 0.109 3.907)", "--color-secondary": "oklch(82% 0.111 230.318)", "--color-secondary-content": "oklch(29% 0.066 243.157)", "--color-accent": "oklch(75% 0.183 55.934)", "--color-accent-content": "oklch(26% 0.079 36.259)", "--color-neutral": "oklch(45% 0.24 277.023)", "--color-neutral-content": "oklch(87% 0.065 274.039)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(77% 0.152 181.912)", "--color-success-content": "oklch(27% 0.046 192.524)", "--color-warning": "oklch(90% 0.182 98.111)", "--color-warning-content": "oklch(42% 0.095 57.708)", "--color-error": "oklch(73.7% 0.121 32.639)", "--color-error-content": "oklch(23.501% 0.096 290.329)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, dim: { "color-scheme": "dark", "--color-base-100": "oklch(30.857% 0.023 264.149)", "--color-base-200": "oklch(28.036% 0.019 264.182)", "--color-base-300": "oklch(26.346% 0.018 262.177)", "--color-base-content": "oklch(82.901% 0.031 222.959)", "--color-primary": "oklch(86.133% 0.141 139.549)", "--color-primary-content": "oklch(17.226% 0.028 139.549)", "--color-secondary": "oklch(73.375% 0.165 35.353)", "--color-secondary-content": "oklch(14.675% 0.033 35.353)", "--color-accent": "oklch(74.229% 0.133 311.379)", "--color-accent-content": "oklch(14.845% 0.026 311.379)", "--color-neutral": "oklch(24.731% 0.02 264.094)", "--color-neutral-content": "oklch(82.901% 0.031 222.959)", "--color-info": "oklch(86.078% 0.142 206.182)", "--color-info-content": "oklch(17.215% 0.028 206.182)", "--color-success": "oklch(86.171% 0.142 166.534)", "--color-success-content": "oklch(17.234% 0.028 166.534)", "--color-warning": "oklch(86.163% 0.142 94.818)", "--color-warning-content": "oklch(17.232% 0.028 94.818)", "--color-error": "oklch(82.418% 0.099 33.756)", "--color-error-content": "oklch(16.483% 0.019 33.756)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, abyss: { "color-scheme": "dark", "--color-base-100": "oklch(20% 0.08 209)", "--color-base-200": "oklch(15% 0.08 209)", "--color-base-300": "oklch(10% 0.08 209)", "--color-base-content": "oklch(90% 0.076 70.697)", "--color-primary": "oklch(92% 0.2653 125)", "--color-primary-content": "oklch(50% 0.2653 125)", "--color-secondary": "oklch(83.27% 0.0764 298.3)", "--color-secondary-content": "oklch(43.27% 0.0764 298.3)", "--color-accent": "oklch(43% 0 0)", "--color-accent-content": "oklch(98% 0 0)", "--color-neutral": "oklch(30% 0.08 209)", "--color-neutral-content": "oklch(90% 0.076 70.697)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(79% 0.209 151.711)", "--color-success-content": "oklch(26% 0.065 152.934)", "--color-warning": "oklch(84.8% 0.1962 84.62)", "--color-warning-content": "oklch(44.8% 0.1962 84.62)", "--color-error": "oklch(65% 0.1985 24.22)", "--color-error-content": "oklch(27% 0.1985 24.22)", "--radius-selector": "2rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, forest: { "color-scheme": "dark", "--color-base-100": "oklch(20.84% 0.008 17.911)", "--color-base-200": "oklch(18.522% 0.007 17.911)", "--color-base-300": "oklch(16.203% 0.007 17.911)", "--color-base-content": "oklch(83.768% 0.001 17.911)", "--color-primary": "oklch(68.628% 0.185 148.958)", "--color-primary-content": "oklch(0% 0 0)", "--color-secondary": "oklch(69.776% 0.135 168.327)", "--color-secondary-content": "oklch(13.955% 0.027 168.327)", "--color-accent": "oklch(70.628% 0.119 185.713)", "--color-accent-content": "oklch(14.125% 0.023 185.713)", "--color-neutral": "oklch(30.698% 0.039 171.364)", "--color-neutral-content": "oklch(86.139% 0.007 171.364)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, night: { "color-scheme": "dark", "--color-base-100": "oklch(20.768% 0.039 265.754)", "--color-base-200": "oklch(19.314% 0.037 265.754)", "--color-base-300": "oklch(17.86% 0.034 265.754)", "--color-base-content": "oklch(84.153% 0.007 265.754)", "--color-primary": "oklch(75.351% 0.138 232.661)", "--color-primary-content": "oklch(15.07% 0.027 232.661)", "--color-secondary": "oklch(68.011% 0.158 276.934)", "--color-secondary-content": "oklch(13.602% 0.031 276.934)", "--color-accent": "oklch(72.36% 0.176 350.048)", "--color-accent-content": "oklch(14.472% 0.035 350.048)", "--color-neutral": "oklch(27.949% 0.036 260.03)", "--color-neutral-content": "oklch(85.589% 0.007 260.03)", "--color-info": "oklch(68.455% 0.148 237.251)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(78.452% 0.132 181.911)", "--color-success-content": "oklch(15.69% 0.026 181.911)", "--color-warning": "oklch(83.242% 0.139 82.95)", "--color-warning-content": "oklch(16.648% 0.027 82.95)", "--color-error": "oklch(71.785% 0.17 13.118)", "--color-error-content": "oklch(14.357% 0.034 13.118)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, caramellatte: { "color-scheme": "light", "--color-base-100": "oklch(98% 0.016 73.684)", "--color-base-200": "oklch(95% 0.038 75.164)", "--color-base-300": "oklch(90% 0.076 70.697)", "--color-base-content": "oklch(40% 0.123 38.172)", "--color-primary": "oklch(0% 0 0)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(22.45% 0.075 37.85)", "--color-secondary-content": "oklch(90% 0.076 70.697)", "--color-accent": "oklch(46.44% 0.111 37.85)", "--color-accent-content": "oklch(90% 0.076 70.697)", "--color-neutral": "oklch(55% 0.195 38.402)", "--color-neutral-content": "oklch(98% 0.016 73.684)", "--color-info": "oklch(42% 0.199 265.638)", "--color-info-content": "oklch(90% 0.076 70.697)", "--color-success": "oklch(43% 0.095 166.913)", "--color-success-content": "oklch(90% 0.076 70.697)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(39% 0.141 25.723)", "--radius-selector": "2rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "1", "--noise": "1" }, autumn: { "color-scheme": "light", "--color-base-100": "oklch(95.814% 0 0)", "--color-base-200": "oklch(89.107% 0 0)", "--color-base-300": "oklch(82.4% 0 0)", "--color-base-content": "oklch(19.162% 0 0)", "--color-primary": "oklch(40.723% 0.161 17.53)", "--color-primary-content": "oklch(88.144% 0.032 17.53)", "--color-secondary": "oklch(61.676% 0.169 23.865)", "--color-secondary-content": "oklch(12.335% 0.033 23.865)", "--color-accent": "oklch(73.425% 0.094 60.729)", "--color-accent-content": "oklch(14.685% 0.018 60.729)", "--color-neutral": "oklch(54.367% 0.037 51.902)", "--color-neutral-content": "oklch(90.873% 0.007 51.902)", "--color-info": "oklch(69.224% 0.097 207.284)", "--color-info-content": "oklch(13.844% 0.019 207.284)", "--color-success": "oklch(60.995% 0.08 174.616)", "--color-success-content": "oklch(12.199% 0.016 174.616)", "--color-warning": "oklch(70.081% 0.164 56.844)", "--color-warning-content": "oklch(14.016% 0.032 56.844)", "--color-error": "oklch(53.07% 0.241 24.16)", "--color-error-content": "oklch(90.614% 0.048 24.16)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, emerald: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(93% 0 0)", "--color-base-300": "oklch(86% 0 0)", "--color-base-content": "oklch(35.519% 0.032 262.988)", "--color-primary": "oklch(76.662% 0.135 153.45)", "--color-primary-content": "oklch(33.387% 0.04 162.24)", "--color-secondary": "oklch(61.302% 0.202 261.294)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(72.772% 0.149 33.2)", "--color-accent-content": "oklch(0% 0 0)", "--color-neutral": "oklch(35.519% 0.032 262.988)", "--color-neutral-content": "oklch(98.462% 0.001 247.838)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, cupcake: { "color-scheme": "light", "--color-base-100": "oklch(97.788% 0.004 56.375)", "--color-base-200": "oklch(93.982% 0.007 61.449)", "--color-base-300": "oklch(91.586% 0.006 53.44)", "--color-base-content": "oklch(23.574% 0.066 313.189)", "--color-primary": "oklch(85% 0.138 181.071)", "--color-primary-content": "oklch(43% 0.078 188.216)", "--color-secondary": "oklch(89% 0.061 343.231)", "--color-secondary-content": "oklch(45% 0.187 3.815)", "--color-accent": "oklch(90% 0.076 70.697)", "--color-accent-content": "oklch(47% 0.157 37.304)", "--color-neutral": "oklch(27% 0.006 286.033)", "--color-neutral-content": "oklch(92% 0.004 286.32)", "--color-info": "oklch(68% 0.169 237.323)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(69% 0.17 162.48)", "--color-success-content": "oklch(26% 0.051 172.552)", "--color-warning": "oklch(79% 0.184 86.047)", "--color-warning-content": "oklch(28% 0.066 53.813)", "--color-error": "oklch(64% 0.246 16.439)", "--color-error-content": "oklch(27% 0.105 12.094)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "1", "--noise": "0" }, cmyk: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(95% 0 0)", "--color-base-300": "oklch(90% 0 0)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(71.772% 0.133 239.443)", "--color-primary-content": "oklch(14.354% 0.026 239.443)", "--color-secondary": "oklch(64.476% 0.202 359.339)", "--color-secondary-content": "oklch(12.895% 0.04 359.339)", "--color-accent": "oklch(94.228% 0.189 105.306)", "--color-accent-content": "oklch(18.845% 0.037 105.306)", "--color-neutral": "oklch(21.778% 0 0)", "--color-neutral-content": "oklch(84.355% 0 0)", "--color-info": "oklch(68.475% 0.094 217.284)", "--color-info-content": "oklch(13.695% 0.018 217.284)", "--color-success": "oklch(46.949% 0.162 321.406)", "--color-success-content": "oklch(89.389% 0.032 321.406)", "--color-warning": "oklch(71.236% 0.159 52.023)", "--color-warning-content": "oklch(14.247% 0.031 52.023)", "--color-error": "oklch(62.013% 0.208 28.717)", "--color-error-content": "oklch(12.402% 0.041 28.717)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, business: { "color-scheme": "dark", "--color-base-100": "oklch(24.353% 0 0)", "--color-base-200": "oklch(22.648% 0 0)", "--color-base-300": "oklch(20.944% 0 0)", "--color-base-content": "oklch(84.87% 0 0)", "--color-primary": "oklch(41.703% 0.099 251.473)", "--color-primary-content": "oklch(88.34% 0.019 251.473)", "--color-secondary": "oklch(64.092% 0.027 229.389)", "--color-secondary-content": "oklch(12.818% 0.005 229.389)", "--color-accent": "oklch(67.271% 0.167 35.791)", "--color-accent-content": "oklch(13.454% 0.033 35.791)", "--color-neutral": "oklch(27.441% 0.013 253.041)", "--color-neutral-content": "oklch(85.488% 0.002 253.041)", "--color-info": "oklch(62.616% 0.143 240.033)", "--color-info-content": "oklch(12.523% 0.028 240.033)", "--color-success": "oklch(70.226% 0.094 156.596)", "--color-success-content": "oklch(14.045% 0.018 156.596)", "--color-warning": "oklch(77.482% 0.115 81.519)", "--color-warning-content": "oklch(15.496% 0.023 81.519)", "--color-error": "oklch(51.61% 0.146 29.674)", "--color-error-content": "oklch(90.322% 0.029 29.674)", "--radius-selector": "0rem", "--radius-field": "0.25rem", "--radius-box": "0.25rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, winter: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97.466% 0.011 259.822)", "--color-base-300": "oklch(93.268% 0.016 262.751)", "--color-base-content": "oklch(41.886% 0.053 255.824)", "--color-primary": "oklch(56.86% 0.255 257.57)", "--color-primary-content": "oklch(91.372% 0.051 257.57)", "--color-secondary": "oklch(42.551% 0.161 282.339)", "--color-secondary-content": "oklch(88.51% 0.032 282.339)", "--color-accent": "oklch(59.939% 0.191 335.171)", "--color-accent-content": "oklch(11.988% 0.038 335.171)", "--color-neutral": "oklch(19.616% 0.063 257.651)", "--color-neutral-content": "oklch(83.923% 0.012 257.651)", "--color-info": "oklch(88.127% 0.085 214.515)", "--color-info-content": "oklch(17.625% 0.017 214.515)", "--color-success": "oklch(80.494% 0.077 197.823)", "--color-success-content": "oklch(16.098% 0.015 197.823)", "--color-warning": "oklch(89.172% 0.045 71.47)", "--color-warning-content": "oklch(17.834% 0.009 71.47)", "--color-error": "oklch(73.092% 0.11 20.076)", "--color-error-content": "oklch(14.618% 0.022 20.076)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, halloween: { "color-scheme": "dark", "--color-base-100": "oklch(21% 0.006 56.043)", "--color-base-200": "oklch(14% 0.004 49.25)", "--color-base-300": "oklch(0% 0 0)", "--color-base-content": "oklch(84.955% 0 0)", "--color-primary": "oklch(77.48% 0.204 60.62)", "--color-primary-content": "oklch(19.693% 0.004 196.779)", "--color-secondary": "oklch(45.98% 0.248 305.03)", "--color-secondary-content": "oklch(89.196% 0.049 305.03)", "--color-accent": "oklch(64.8% 0.223 136.073)", "--color-accent-content": "oklch(0% 0 0)", "--color-neutral": "oklch(24.371% 0.046 65.681)", "--color-neutral-content": "oklch(84.874% 0.009 65.681)", "--color-info": "oklch(54.615% 0.215 262.88)", "--color-info-content": "oklch(90.923% 0.043 262.88)", "--color-success": "oklch(62.705% 0.169 149.213)", "--color-success-content": "oklch(12.541% 0.033 149.213)", "--color-warning": "oklch(66.584% 0.157 58.318)", "--color-warning-content": "oklch(13.316% 0.031 58.318)", "--color-error": "oklch(65.72% 0.199 27.33)", "--color-error-content": "oklch(13.144% 0.039 27.33)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, fantasy: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(93% 0 0)", "--color-base-300": "oklch(86% 0 0)", "--color-base-content": "oklch(27.807% 0.029 256.847)", "--color-primary": "oklch(37.45% 0.189 325.02)", "--color-primary-content": "oklch(87.49% 0.037 325.02)", "--color-secondary": "oklch(53.92% 0.162 241.36)", "--color-secondary-content": "oklch(90.784% 0.032 241.36)", "--color-accent": "oklch(75.98% 0.204 56.72)", "--color-accent-content": "oklch(15.196% 0.04 56.72)", "--color-neutral": "oklch(27.807% 0.029 256.847)", "--color-neutral-content": "oklch(85.561% 0.005 256.847)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, wireframe: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97% 0 0)", "--color-base-300": "oklch(94% 0 0)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(87% 0 0)", "--color-primary-content": "oklch(26% 0 0)", "--color-secondary": "oklch(87% 0 0)", "--color-secondary-content": "oklch(26% 0 0)", "--color-accent": "oklch(87% 0 0)", "--color-accent-content": "oklch(26% 0 0)", "--color-neutral": "oklch(87% 0 0)", "--color-neutral-content": "oklch(26% 0 0)", "--color-info": "oklch(44% 0.11 240.79)", "--color-info-content": "oklch(90% 0.058 230.902)", "--color-success": "oklch(43% 0.095 166.913)", "--color-success-content": "oklch(90% 0.093 164.15)", "--color-warning": "oklch(47% 0.137 46.201)", "--color-warning-content": "oklch(92% 0.12 95.746)", "--color-error": "oklch(44% 0.177 26.899)", "--color-error-content": "oklch(88% 0.062 18.334)", "--radius-selector": "0rem", "--radius-field": "0.25rem", "--radius-box": "0.25rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" } };
+
+// packages/daisyui/theme/index.js
+var theme_default = plugin.withOptions((options = {}) => {
+ return ({ addBase }) => {
+ const {
+ name = "custom-theme",
+ default: isDefault = false,
+ prefersdark = false,
+ "color-scheme": colorScheme = "normal",
+ root = ":root",
+ ...customThemeTokens
+ } = options;
+ let selector = `${root}:has(input.theme-controller[value=${name}]:checked),[data-theme="${name}"]`;
+ if (isDefault) {
+ selector = `:where(${root}),${selector}`;
+ }
+ let themeTokens = { ...customThemeTokens };
+ if (object_default[name]) {
+ const builtinTheme = object_default[name];
+ themeTokens = {
+ ...builtinTheme,
+ ...customThemeTokens,
+ "color-scheme": colorScheme || builtinTheme.colorScheme
+ };
+ }
+ const baseStyles = {
+ [selector]: {
+ "color-scheme": themeTokens["color-scheme"] || colorScheme,
+ ...themeTokens
+ }
+ };
+ if (prefersdark) {
+ addBase({
+ "@media (prefers-color-scheme: dark)": {
+ [root]: baseStyles[selector]
+ }
+ });
+ }
+ addBase(baseStyles);
+ };
+});
+
+
+/*
+
+ MIT License
+
+ Copyright (c) 2020 Pouya Saadeghi – https://daisyui.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+*/
diff --git a/assets/vendor/daisyui.js b/assets/vendor/daisyui.js
@@ -0,0 +1,1031 @@
+/** 🌼
+ * @license MIT
+ * daisyUI bundle
+ * https://daisyui.com/
+ */
+
+var __defProp = Object.defineProperty;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __moduleCache = /* @__PURE__ */ new WeakMap;
+var __toCommonJS = (from) => {
+ var entry = __moduleCache.get(from), desc;
+ if (entry)
+ return entry;
+ entry = __defProp({}, "__esModule", { value: true });
+ if (from && typeof from === "object" || typeof from === "function")
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
+ get: () => from[key],
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
+ }));
+ __moduleCache.set(from, entry);
+ return entry;
+};
+var __export = (target, all) => {
+ for (var name in all)
+ __defProp(target, name, {
+ get: all[name],
+ enumerable: true,
+ configurable: true,
+ set: (newValue) => all[name] = () => newValue
+ });
+};
+
+// packages/daisyui/index.js
+var exports_daisyui = {};
+__export(exports_daisyui, {
+ default: () => daisyui_default
+});
+module.exports = __toCommonJS(exports_daisyui);
+
+// packages/daisyui/functions/themeOrder.js
+var themeOrder_default = [
+ "light",
+ "dark",
+ "cupcake",
+ "bumblebee",
+ "emerald",
+ "corporate",
+ "synthwave",
+ "retro",
+ "cyberpunk",
+ "valentine",
+ "halloween",
+ "garden",
+ "forest",
+ "aqua",
+ "lofi",
+ "pastel",
+ "fantasy",
+ "wireframe",
+ "black",
+ "luxury",
+ "dracula",
+ "cmyk",
+ "autumn",
+ "business",
+ "acid",
+ "lemonade",
+ "night",
+ "coffee",
+ "winter",
+ "dim",
+ "nord",
+ "sunset",
+ "caramellatte",
+ "abyss",
+ "silk"
+];
+
+// packages/daisyui/functions/pluginOptionsHandler.js
+var pluginOptionsHandler = (() => {
+ let firstRun = true;
+ return (options, addBase, themesObject, packageVersion) => {
+ const {
+ logs = true,
+ root = ":root",
+ themes = ["light --default", "dark --prefersdark"],
+ include,
+ exclude,
+ prefix = ""
+ } = options || {};
+ if (logs !== false && firstRun) {
+ console.log(`${atob("Lyoh")} ${decodeURIComponent("%F0%9F%8C%BC")} ${atob("ZGFpc3lVSQ==")} ${packageVersion} ${atob("Ki8=")}`);
+ firstRun = false;
+ }
+ const applyTheme = (themeName, flags) => {
+ const theme = themesObject[themeName];
+ if (theme) {
+ let selector = `${root}:has(input.theme-controller[value=${themeName}]:checked),[data-theme=${themeName}]`;
+ if (flags.includes("--default")) {
+ selector = `:where(${root}),${selector}`;
+ }
+ addBase({ [selector]: theme });
+ if (flags.includes("--prefersdark")) {
+ addBase({ "@media (prefers-color-scheme: dark)": { [root]: theme } });
+ }
+ }
+ };
+ if (themes === "all") {
+ if (themesObject["light"]) {
+ applyTheme("light", ["--default"]);
+ }
+ if (themesObject["dark"]) {
+ addBase({ "@media (prefers-color-scheme: dark)": { [root]: themesObject["dark"] } });
+ }
+ themeOrder_default.forEach((themeName) => {
+ if (themesObject[themeName]) {
+ applyTheme(themeName, []);
+ }
+ });
+ } else if (themes) {
+ const themeArray = Array.isArray(themes) ? themes : [themes];
+ if (themeArray.length === 1 && themeArray[0].includes("--default")) {
+ const [themeName, ...flags] = themeArray[0].split(" ");
+ applyTheme(themeName, flags);
+ return { include, exclude, prefix };
+ }
+ themeArray.forEach((themeOption) => {
+ const [themeName, ...flags] = themeOption.split(" ");
+ if (flags.includes("--default")) {
+ applyTheme(themeName, ["--default"]);
+ }
+ });
+ themeArray.forEach((themeOption) => {
+ const [themeName, ...flags] = themeOption.split(" ");
+ if (flags.includes("--prefersdark")) {
+ addBase({ "@media (prefers-color-scheme: dark)": { [root]: themesObject[themeName] } });
+ }
+ });
+ themeArray.forEach((themeOption) => {
+ const [themeName] = themeOption.split(" ");
+ applyTheme(themeName, []);
+ });
+ }
+ return { include, exclude, prefix };
+ };
+})();
+
+// packages/daisyui/functions/plugin.js
+var plugin = {
+ withOptions: (pluginFunction, configFunction = () => ({})) => {
+ const optionsFunction = (options) => {
+ const handler = pluginFunction(options);
+ const config = configFunction(options);
+ return { handler, config };
+ };
+ optionsFunction.__isOptionsFunction = true;
+ return optionsFunction;
+ }
+};
+
+// packages/daisyui/functions/variables.js
+var variables_default = {
+ colors: {
+ "base-100": "var(--color-base-100)",
+ "base-200": "var(--color-base-200)",
+ "base-300": "var(--color-base-300)",
+ "base-content": "var(--color-base-content)",
+ primary: "var(--color-primary)",
+ "primary-content": "var(--color-primary-content)",
+ secondary: "var(--color-secondary)",
+ "secondary-content": "var(--color-secondary-content)",
+ accent: "var(--color-accent)",
+ "accent-content": "var(--color-accent-content)",
+ neutral: "var(--color-neutral)",
+ "neutral-content": "var(--color-neutral-content)",
+ info: "var(--color-info)",
+ "info-content": "var(--color-info-content)",
+ success: "var(--color-success)",
+ "success-content": "var(--color-success-content)",
+ warning: "var(--color-warning)",
+ "warning-content": "var(--color-warning-content)",
+ error: "var(--color-error)",
+ "error-content": "var(--color-error-content)"
+ },
+ borderRadius: {
+ selector: "var(--radius-selector)",
+ field: "var(--radius-field)",
+ box: "var(--radius-box)"
+ }
+};
+
+// packages/daisyui/theme/object.js
+var object_default = { cyberpunk: { "color-scheme": "light", "--color-base-100": "oklch(94.51% 0.179 104.32)", "--color-base-200": "oklch(91.51% 0.179 104.32)", "--color-base-300": "oklch(85.51% 0.179 104.32)", "--color-base-content": "oklch(0% 0 0)", "--color-primary": "oklch(74.22% 0.209 6.35)", "--color-primary-content": "oklch(14.844% 0.041 6.35)", "--color-secondary": "oklch(83.33% 0.184 204.72)", "--color-secondary-content": "oklch(16.666% 0.036 204.72)", "--color-accent": "oklch(71.86% 0.217 310.43)", "--color-accent-content": "oklch(14.372% 0.043 310.43)", "--color-neutral": "oklch(23.04% 0.065 269.31)", "--color-neutral-content": "oklch(94.51% 0.179 104.32)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "0rem", "--radius-field": "0rem", "--radius-box": "0rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, acid: { "color-scheme": "light", "--color-base-100": "oklch(98% 0 0)", "--color-base-200": "oklch(95% 0 0)", "--color-base-300": "oklch(91% 0 0)", "--color-base-content": "oklch(0% 0 0)", "--color-primary": "oklch(71.9% 0.357 330.759)", "--color-primary-content": "oklch(14.38% 0.071 330.759)", "--color-secondary": "oklch(73.37% 0.224 48.25)", "--color-secondary-content": "oklch(14.674% 0.044 48.25)", "--color-accent": "oklch(92.78% 0.264 122.962)", "--color-accent-content": "oklch(18.556% 0.052 122.962)", "--color-neutral": "oklch(21.31% 0.128 278.68)", "--color-neutral-content": "oklch(84.262% 0.025 278.68)", "--color-info": "oklch(60.72% 0.227 252.05)", "--color-info-content": "oklch(12.144% 0.045 252.05)", "--color-success": "oklch(85.72% 0.266 158.53)", "--color-success-content": "oklch(17.144% 0.053 158.53)", "--color-warning": "oklch(91.01% 0.212 100.5)", "--color-warning-content": "oklch(18.202% 0.042 100.5)", "--color-error": "oklch(64.84% 0.293 29.349)", "--color-error-content": "oklch(12.968% 0.058 29.349)", "--radius-selector": "1rem", "--radius-field": "1rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, black: { "color-scheme": "dark", "--color-base-100": "oklch(0% 0 0)", "--color-base-200": "oklch(19% 0 0)", "--color-base-300": "oklch(22% 0 0)", "--color-base-content": "oklch(87.609% 0 0)", "--color-primary": "oklch(35% 0 0)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(35% 0 0)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(35% 0 0)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(35% 0 0)", "--color-neutral-content": "oklch(100% 0 0)", "--color-info": "oklch(45.201% 0.313 264.052)", "--color-info-content": "oklch(89.04% 0.062 264.052)", "--color-success": "oklch(51.975% 0.176 142.495)", "--color-success-content": "oklch(90.395% 0.035 142.495)", "--color-warning": "oklch(96.798% 0.211 109.769)", "--color-warning-content": "oklch(19.359% 0.042 109.769)", "--color-error": "oklch(62.795% 0.257 29.233)", "--color-error-content": "oklch(12.559% 0.051 29.233)", "--radius-selector": "0rem", "--radius-field": "0rem", "--radius-box": "0rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, dark: { "color-scheme": "dark", "--color-base-100": "oklch(25.33% 0.016 252.42)", "--color-base-200": "oklch(23.26% 0.014 253.1)", "--color-base-300": "oklch(21.15% 0.012 254.09)", "--color-base-content": "oklch(97.807% 0.029 256.847)", "--color-primary": "oklch(58% 0.233 277.117)", "--color-primary-content": "oklch(96% 0.018 272.314)", "--color-secondary": "oklch(65% 0.241 354.308)", "--color-secondary-content": "oklch(94% 0.028 342.258)", "--color-accent": "oklch(77% 0.152 181.912)", "--color-accent-content": "oklch(38% 0.063 188.416)", "--color-neutral": "oklch(14% 0.005 285.823)", "--color-neutral-content": "oklch(92% 0.004 286.32)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(76% 0.177 163.223)", "--color-success-content": "oklch(37% 0.077 168.94)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(71% 0.194 13.428)", "--color-error-content": "oklch(27% 0.105 12.094)", "--radius-selector": "0.5rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, light: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(98% 0 0)", "--color-base-300": "oklch(95% 0 0)", "--color-base-content": "oklch(21% 0.006 285.885)", "--color-primary": "oklch(45% 0.24 277.023)", "--color-primary-content": "oklch(93% 0.034 272.788)", "--color-secondary": "oklch(65% 0.241 354.308)", "--color-secondary-content": "oklch(94% 0.028 342.258)", "--color-accent": "oklch(77% 0.152 181.912)", "--color-accent-content": "oklch(38% 0.063 188.416)", "--color-neutral": "oklch(14% 0.005 285.823)", "--color-neutral-content": "oklch(92% 0.004 286.32)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(76% 0.177 163.223)", "--color-success-content": "oklch(37% 0.077 168.94)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(71% 0.194 13.428)", "--color-error-content": "oklch(27% 0.105 12.094)", "--radius-selector": "0.5rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, luxury: { "color-scheme": "dark", "--color-base-100": "oklch(14.076% 0.004 285.822)", "--color-base-200": "oklch(20.219% 0.004 308.229)", "--color-base-300": "oklch(23.219% 0.004 308.229)", "--color-base-content": "oklch(75.687% 0.123 76.89)", "--color-primary": "oklch(100% 0 0)", "--color-primary-content": "oklch(20% 0 0)", "--color-secondary": "oklch(27.581% 0.064 261.069)", "--color-secondary-content": "oklch(85.516% 0.012 261.069)", "--color-accent": "oklch(36.674% 0.051 338.825)", "--color-accent-content": "oklch(87.334% 0.01 338.825)", "--color-neutral": "oklch(24.27% 0.057 59.825)", "--color-neutral-content": "oklch(93.203% 0.089 90.861)", "--color-info": "oklch(79.061% 0.121 237.133)", "--color-info-content": "oklch(15.812% 0.024 237.133)", "--color-success": "oklch(78.119% 0.192 132.154)", "--color-success-content": "oklch(15.623% 0.038 132.154)", "--color-warning": "oklch(86.127% 0.136 102.891)", "--color-warning-content": "oklch(17.225% 0.027 102.891)", "--color-error": "oklch(71.753% 0.176 22.568)", "--color-error-content": "oklch(14.35% 0.035 22.568)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, dracula: { "color-scheme": "dark", "--color-base-100": "oklch(28.822% 0.022 277.508)", "--color-base-200": "oklch(26.805% 0.02 277.508)", "--color-base-300": "oklch(24.787% 0.019 277.508)", "--color-base-content": "oklch(97.747% 0.007 106.545)", "--color-primary": "oklch(75.461% 0.183 346.812)", "--color-primary-content": "oklch(15.092% 0.036 346.812)", "--color-secondary": "oklch(74.202% 0.148 301.883)", "--color-secondary-content": "oklch(14.84% 0.029 301.883)", "--color-accent": "oklch(83.392% 0.124 66.558)", "--color-accent-content": "oklch(16.678% 0.024 66.558)", "--color-neutral": "oklch(39.445% 0.032 275.524)", "--color-neutral-content": "oklch(87.889% 0.006 275.524)", "--color-info": "oklch(88.263% 0.093 212.846)", "--color-info-content": "oklch(17.652% 0.018 212.846)", "--color-success": "oklch(87.099% 0.219 148.024)", "--color-success-content": "oklch(17.419% 0.043 148.024)", "--color-warning": "oklch(95.533% 0.134 112.757)", "--color-warning-content": "oklch(19.106% 0.026 112.757)", "--color-error": "oklch(68.22% 0.206 24.43)", "--color-error-content": "oklch(13.644% 0.041 24.43)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, retro: { "color-scheme": "light", "--color-base-100": "oklch(91.637% 0.034 90.515)", "--color-base-200": "oklch(88.272% 0.049 91.774)", "--color-base-300": "oklch(84.133% 0.065 90.856)", "--color-base-content": "oklch(41% 0.112 45.904)", "--color-primary": "oklch(80% 0.114 19.571)", "--color-primary-content": "oklch(39% 0.141 25.723)", "--color-secondary": "oklch(92% 0.084 155.995)", "--color-secondary-content": "oklch(44% 0.119 151.328)", "--color-accent": "oklch(68% 0.162 75.834)", "--color-accent-content": "oklch(41% 0.112 45.904)", "--color-neutral": "oklch(44% 0.011 73.639)", "--color-neutral-content": "oklch(86% 0.005 56.366)", "--color-info": "oklch(58% 0.158 241.966)", "--color-info-content": "oklch(96% 0.059 95.617)", "--color-success": "oklch(51% 0.096 186.391)", "--color-success-content": "oklch(96% 0.059 95.617)", "--color-warning": "oklch(64% 0.222 41.116)", "--color-warning-content": "oklch(96% 0.059 95.617)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(40% 0.123 38.172)", "--radius-selector": "0.25rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, lofi: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97% 0 0)", "--color-base-300": "oklch(94% 0 0)", "--color-base-content": "oklch(0% 0 0)", "--color-primary": "oklch(15.906% 0 0)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(21.455% 0.001 17.278)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(26.861% 0 0)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(0% 0 0)", "--color-neutral-content": "oklch(100% 0 0)", "--color-info": "oklch(79.54% 0.103 205.9)", "--color-info-content": "oklch(15.908% 0.02 205.9)", "--color-success": "oklch(90.13% 0.153 164.14)", "--color-success-content": "oklch(18.026% 0.03 164.14)", "--color-warning": "oklch(88.37% 0.135 79.94)", "--color-warning-content": "oklch(17.674% 0.027 79.94)", "--color-error": "oklch(78.66% 0.15 28.47)", "--color-error-content": "oklch(15.732% 0.03 28.47)", "--radius-selector": "2rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, valentine: { "color-scheme": "light", "--color-base-100": "oklch(97% 0.014 343.198)", "--color-base-200": "oklch(94% 0.028 342.258)", "--color-base-300": "oklch(89% 0.061 343.231)", "--color-base-content": "oklch(52% 0.223 3.958)", "--color-primary": "oklch(65% 0.241 354.308)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(62% 0.265 303.9)", "--color-secondary-content": "oklch(97% 0.014 308.299)", "--color-accent": "oklch(82% 0.111 230.318)", "--color-accent-content": "oklch(39% 0.09 240.876)", "--color-neutral": "oklch(40% 0.153 2.432)", "--color-neutral-content": "oklch(89% 0.061 343.231)", "--color-info": "oklch(86% 0.127 207.078)", "--color-info-content": "oklch(44% 0.11 240.79)", "--color-success": "oklch(84% 0.143 164.978)", "--color-success-content": "oklch(43% 0.095 166.913)", "--color-warning": "oklch(75% 0.183 55.934)", "--color-warning-content": "oklch(26% 0.079 36.259)", "--color-error": "oklch(63% 0.237 25.331)", "--color-error-content": "oklch(97% 0.013 17.38)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, nord: { "color-scheme": "light", "--color-base-100": "oklch(95.127% 0.007 260.731)", "--color-base-200": "oklch(93.299% 0.01 261.788)", "--color-base-300": "oklch(89.925% 0.016 262.749)", "--color-base-content": "oklch(32.437% 0.022 264.182)", "--color-primary": "oklch(59.435% 0.077 254.027)", "--color-primary-content": "oklch(11.887% 0.015 254.027)", "--color-secondary": "oklch(69.651% 0.059 248.687)", "--color-secondary-content": "oklch(13.93% 0.011 248.687)", "--color-accent": "oklch(77.464% 0.062 217.469)", "--color-accent-content": "oklch(15.492% 0.012 217.469)", "--color-neutral": "oklch(45.229% 0.035 264.131)", "--color-neutral-content": "oklch(89.925% 0.016 262.749)", "--color-info": "oklch(69.207% 0.062 332.664)", "--color-info-content": "oklch(13.841% 0.012 332.664)", "--color-success": "oklch(76.827% 0.074 131.063)", "--color-success-content": "oklch(15.365% 0.014 131.063)", "--color-warning": "oklch(85.486% 0.089 84.093)", "--color-warning-content": "oklch(17.097% 0.017 84.093)", "--color-error": "oklch(60.61% 0.12 15.341)", "--color-error-content": "oklch(12.122% 0.024 15.341)", "--radius-selector": "1rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, lemonade: { "color-scheme": "light", "--color-base-100": "oklch(98.71% 0.02 123.72)", "--color-base-200": "oklch(91.8% 0.018 123.72)", "--color-base-300": "oklch(84.89% 0.017 123.72)", "--color-base-content": "oklch(19.742% 0.004 123.72)", "--color-primary": "oklch(58.92% 0.199 134.6)", "--color-primary-content": "oklch(11.784% 0.039 134.6)", "--color-secondary": "oklch(77.75% 0.196 111.09)", "--color-secondary-content": "oklch(15.55% 0.039 111.09)", "--color-accent": "oklch(85.39% 0.201 100.73)", "--color-accent-content": "oklch(17.078% 0.04 100.73)", "--color-neutral": "oklch(30.98% 0.075 108.6)", "--color-neutral-content": "oklch(86.196% 0.015 108.6)", "--color-info": "oklch(86.19% 0.047 224.14)", "--color-info-content": "oklch(17.238% 0.009 224.14)", "--color-success": "oklch(86.19% 0.047 157.85)", "--color-success-content": "oklch(17.238% 0.009 157.85)", "--color-warning": "oklch(86.19% 0.047 102.15)", "--color-warning-content": "oklch(17.238% 0.009 102.15)", "--color-error": "oklch(86.19% 0.047 25.85)", "--color-error-content": "oklch(17.238% 0.009 25.85)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, garden: { "color-scheme": "light", "--color-base-100": "oklch(92.951% 0.002 17.197)", "--color-base-200": "oklch(86.445% 0.002 17.197)", "--color-base-300": "oklch(79.938% 0.001 17.197)", "--color-base-content": "oklch(16.961% 0.001 17.32)", "--color-primary": "oklch(62.45% 0.278 3.836)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(48.495% 0.11 355.095)", "--color-secondary-content": "oklch(89.699% 0.022 355.095)", "--color-accent": "oklch(56.273% 0.054 154.39)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(24.155% 0.049 89.07)", "--color-neutral-content": "oklch(92.951% 0.002 17.197)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, aqua: { "color-scheme": "dark", "--color-base-100": "oklch(37% 0.146 265.522)", "--color-base-200": "oklch(28% 0.091 267.935)", "--color-base-300": "oklch(22% 0.091 267.935)", "--color-base-content": "oklch(90% 0.058 230.902)", "--color-primary": "oklch(85.661% 0.144 198.645)", "--color-primary-content": "oklch(40.124% 0.068 197.603)", "--color-secondary": "oklch(60.682% 0.108 309.782)", "--color-secondary-content": "oklch(96% 0.016 293.756)", "--color-accent": "oklch(93.426% 0.102 94.555)", "--color-accent-content": "oklch(18.685% 0.02 94.555)", "--color-neutral": "oklch(27% 0.146 265.522)", "--color-neutral-content": "oklch(80% 0.146 265.522)", "--color-info": "oklch(54.615% 0.215 262.88)", "--color-info-content": "oklch(90.923% 0.043 262.88)", "--color-success": "oklch(62.705% 0.169 149.213)", "--color-success-content": "oklch(12.541% 0.033 149.213)", "--color-warning": "oklch(66.584% 0.157 58.318)", "--color-warning-content": "oklch(27% 0.077 45.635)", "--color-error": "oklch(73.95% 0.19 27.33)", "--color-error-content": "oklch(14.79% 0.038 27.33)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, corporate: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(93% 0 0)", "--color-base-300": "oklch(86% 0 0)", "--color-base-content": "oklch(22.389% 0.031 278.072)", "--color-primary": "oklch(58% 0.158 241.966)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(55% 0.046 257.417)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(60% 0.118 184.704)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(0% 0 0)", "--color-neutral-content": "oklch(100% 0 0)", "--color-info": "oklch(60% 0.126 221.723)", "--color-info-content": "oklch(100% 0 0)", "--color-success": "oklch(62% 0.194 149.214)", "--color-success-content": "oklch(100% 0 0)", "--color-warning": "oklch(85% 0.199 91.936)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "0.25rem", "--radius-field": "0.25rem", "--radius-box": "0.25rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, pastel: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(98.462% 0.001 247.838)", "--color-base-300": "oklch(92.462% 0.001 247.838)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(90% 0.063 306.703)", "--color-primary-content": "oklch(49% 0.265 301.924)", "--color-secondary": "oklch(89% 0.058 10.001)", "--color-secondary-content": "oklch(51% 0.222 16.935)", "--color-accent": "oklch(90% 0.093 164.15)", "--color-accent-content": "oklch(50% 0.118 165.612)", "--color-neutral": "oklch(55% 0.046 257.417)", "--color-neutral-content": "oklch(92% 0.013 255.508)", "--color-info": "oklch(86% 0.127 207.078)", "--color-info-content": "oklch(52% 0.105 223.128)", "--color-success": "oklch(87% 0.15 154.449)", "--color-success-content": "oklch(52% 0.154 150.069)", "--color-warning": "oklch(83% 0.128 66.29)", "--color-warning-content": "oklch(55% 0.195 38.402)", "--color-error": "oklch(80% 0.114 19.571)", "--color-error-content": "oklch(50% 0.213 27.518)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "0", "--noise": "0" }, bumblebee: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97% 0 0)", "--color-base-300": "oklch(92% 0 0)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(85% 0.199 91.936)", "--color-primary-content": "oklch(42% 0.095 57.708)", "--color-secondary": "oklch(75% 0.183 55.934)", "--color-secondary-content": "oklch(40% 0.123 38.172)", "--color-accent": "oklch(0% 0 0)", "--color-accent-content": "oklch(100% 0 0)", "--color-neutral": "oklch(37% 0.01 67.558)", "--color-neutral-content": "oklch(92% 0.003 48.717)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(39% 0.09 240.876)", "--color-success": "oklch(76% 0.177 163.223)", "--color-success-content": "oklch(37% 0.077 168.94)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(39% 0.141 25.723)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, coffee: { "color-scheme": "dark", "--color-base-100": "oklch(24% 0.023 329.708)", "--color-base-200": "oklch(21% 0.021 329.708)", "--color-base-300": "oklch(16% 0.019 329.708)", "--color-base-content": "oklch(72.354% 0.092 79.129)", "--color-primary": "oklch(71.996% 0.123 62.756)", "--color-primary-content": "oklch(14.399% 0.024 62.756)", "--color-secondary": "oklch(34.465% 0.029 199.194)", "--color-secondary-content": "oklch(86.893% 0.005 199.194)", "--color-accent": "oklch(42.621% 0.074 224.389)", "--color-accent-content": "oklch(88.524% 0.014 224.389)", "--color-neutral": "oklch(16.51% 0.015 326.261)", "--color-neutral-content": "oklch(83.302% 0.003 326.261)", "--color-info": "oklch(79.49% 0.063 184.558)", "--color-info-content": "oklch(15.898% 0.012 184.558)", "--color-success": "oklch(74.722% 0.072 131.116)", "--color-success-content": "oklch(14.944% 0.014 131.116)", "--color-warning": "oklch(88.15% 0.14 87.722)", "--color-warning-content": "oklch(17.63% 0.028 87.722)", "--color-error": "oklch(77.318% 0.128 31.871)", "--color-error-content": "oklch(15.463% 0.025 31.871)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, silk: { "color-scheme": "light", "--color-base-100": "oklch(97% 0.0035 67.78)", "--color-base-200": "oklch(95% 0.0081 61.42)", "--color-base-300": "oklch(90% 0.0081 61.42)", "--color-base-content": "oklch(40% 0.0081 61.42)", "--color-primary": "oklch(23.27% 0.0249 284.3)", "--color-primary-content": "oklch(94.22% 0.2505 117.44)", "--color-secondary": "oklch(23.27% 0.0249 284.3)", "--color-secondary-content": "oklch(73.92% 0.2135 50.94)", "--color-accent": "oklch(23.27% 0.0249 284.3)", "--color-accent-content": "oklch(88.92% 0.2061 189.9)", "--color-neutral": "oklch(20% 0 0)", "--color-neutral-content": "oklch(80% 0.0081 61.42)", "--color-info": "oklch(80.39% 0.1148 241.68)", "--color-info-content": "oklch(30.39% 0.1148 241.68)", "--color-success": "oklch(83.92% 0.0901 136.87)", "--color-success-content": "oklch(23.92% 0.0901 136.87)", "--color-warning": "oklch(83.92% 0.1085 80)", "--color-warning-content": "oklch(43.92% 0.1085 80)", "--color-error": "oklch(75.1% 0.1814 22.37)", "--color-error-content": "oklch(35.1% 0.1814 22.37)", "--radius-selector": "2rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "1", "--noise": "0" }, sunset: { "color-scheme": "dark", "--color-base-100": "oklch(22% 0.019 237.69)", "--color-base-200": "oklch(20% 0.019 237.69)", "--color-base-300": "oklch(18% 0.019 237.69)", "--color-base-content": "oklch(77.383% 0.043 245.096)", "--color-primary": "oklch(74.703% 0.158 39.947)", "--color-primary-content": "oklch(14.94% 0.031 39.947)", "--color-secondary": "oklch(72.537% 0.177 2.72)", "--color-secondary-content": "oklch(14.507% 0.035 2.72)", "--color-accent": "oklch(71.294% 0.166 299.844)", "--color-accent-content": "oklch(14.258% 0.033 299.844)", "--color-neutral": "oklch(26% 0.019 237.69)", "--color-neutral-content": "oklch(70% 0.019 237.69)", "--color-info": "oklch(85.559% 0.085 206.015)", "--color-info-content": "oklch(17.111% 0.017 206.015)", "--color-success": "oklch(85.56% 0.085 144.778)", "--color-success-content": "oklch(17.112% 0.017 144.778)", "--color-warning": "oklch(85.569% 0.084 74.427)", "--color-warning-content": "oklch(17.113% 0.016 74.427)", "--color-error": "oklch(85.511% 0.078 16.886)", "--color-error-content": "oklch(17.102% 0.015 16.886)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, synthwave: { "color-scheme": "dark", "--color-base-100": "oklch(15% 0.09 281.288)", "--color-base-200": "oklch(20% 0.09 281.288)", "--color-base-300": "oklch(25% 0.09 281.288)", "--color-base-content": "oklch(78% 0.115 274.713)", "--color-primary": "oklch(71% 0.202 349.761)", "--color-primary-content": "oklch(28% 0.109 3.907)", "--color-secondary": "oklch(82% 0.111 230.318)", "--color-secondary-content": "oklch(29% 0.066 243.157)", "--color-accent": "oklch(75% 0.183 55.934)", "--color-accent-content": "oklch(26% 0.079 36.259)", "--color-neutral": "oklch(45% 0.24 277.023)", "--color-neutral-content": "oklch(87% 0.065 274.039)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(77% 0.152 181.912)", "--color-success-content": "oklch(27% 0.046 192.524)", "--color-warning": "oklch(90% 0.182 98.111)", "--color-warning-content": "oklch(42% 0.095 57.708)", "--color-error": "oklch(73.7% 0.121 32.639)", "--color-error-content": "oklch(23.501% 0.096 290.329)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, dim: { "color-scheme": "dark", "--color-base-100": "oklch(30.857% 0.023 264.149)", "--color-base-200": "oklch(28.036% 0.019 264.182)", "--color-base-300": "oklch(26.346% 0.018 262.177)", "--color-base-content": "oklch(82.901% 0.031 222.959)", "--color-primary": "oklch(86.133% 0.141 139.549)", "--color-primary-content": "oklch(17.226% 0.028 139.549)", "--color-secondary": "oklch(73.375% 0.165 35.353)", "--color-secondary-content": "oklch(14.675% 0.033 35.353)", "--color-accent": "oklch(74.229% 0.133 311.379)", "--color-accent-content": "oklch(14.845% 0.026 311.379)", "--color-neutral": "oklch(24.731% 0.02 264.094)", "--color-neutral-content": "oklch(82.901% 0.031 222.959)", "--color-info": "oklch(86.078% 0.142 206.182)", "--color-info-content": "oklch(17.215% 0.028 206.182)", "--color-success": "oklch(86.171% 0.142 166.534)", "--color-success-content": "oklch(17.234% 0.028 166.534)", "--color-warning": "oklch(86.163% 0.142 94.818)", "--color-warning-content": "oklch(17.232% 0.028 94.818)", "--color-error": "oklch(82.418% 0.099 33.756)", "--color-error-content": "oklch(16.483% 0.019 33.756)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, abyss: { "color-scheme": "dark", "--color-base-100": "oklch(20% 0.08 209)", "--color-base-200": "oklch(15% 0.08 209)", "--color-base-300": "oklch(10% 0.08 209)", "--color-base-content": "oklch(90% 0.076 70.697)", "--color-primary": "oklch(92% 0.2653 125)", "--color-primary-content": "oklch(50% 0.2653 125)", "--color-secondary": "oklch(83.27% 0.0764 298.3)", "--color-secondary-content": "oklch(43.27% 0.0764 298.3)", "--color-accent": "oklch(43% 0 0)", "--color-accent-content": "oklch(98% 0 0)", "--color-neutral": "oklch(30% 0.08 209)", "--color-neutral-content": "oklch(90% 0.076 70.697)", "--color-info": "oklch(74% 0.16 232.661)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(79% 0.209 151.711)", "--color-success-content": "oklch(26% 0.065 152.934)", "--color-warning": "oklch(84.8% 0.1962 84.62)", "--color-warning-content": "oklch(44.8% 0.1962 84.62)", "--color-error": "oklch(65% 0.1985 24.22)", "--color-error-content": "oklch(27% 0.1985 24.22)", "--radius-selector": "2rem", "--radius-field": "0.25rem", "--radius-box": "0.5rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, forest: { "color-scheme": "dark", "--color-base-100": "oklch(20.84% 0.008 17.911)", "--color-base-200": "oklch(18.522% 0.007 17.911)", "--color-base-300": "oklch(16.203% 0.007 17.911)", "--color-base-content": "oklch(83.768% 0.001 17.911)", "--color-primary": "oklch(68.628% 0.185 148.958)", "--color-primary-content": "oklch(0% 0 0)", "--color-secondary": "oklch(69.776% 0.135 168.327)", "--color-secondary-content": "oklch(13.955% 0.027 168.327)", "--color-accent": "oklch(70.628% 0.119 185.713)", "--color-accent-content": "oklch(14.125% 0.023 185.713)", "--color-neutral": "oklch(30.698% 0.039 171.364)", "--color-neutral-content": "oklch(86.139% 0.007 171.364)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, night: { "color-scheme": "dark", "--color-base-100": "oklch(20.768% 0.039 265.754)", "--color-base-200": "oklch(19.314% 0.037 265.754)", "--color-base-300": "oklch(17.86% 0.034 265.754)", "--color-base-content": "oklch(84.153% 0.007 265.754)", "--color-primary": "oklch(75.351% 0.138 232.661)", "--color-primary-content": "oklch(15.07% 0.027 232.661)", "--color-secondary": "oklch(68.011% 0.158 276.934)", "--color-secondary-content": "oklch(13.602% 0.031 276.934)", "--color-accent": "oklch(72.36% 0.176 350.048)", "--color-accent-content": "oklch(14.472% 0.035 350.048)", "--color-neutral": "oklch(27.949% 0.036 260.03)", "--color-neutral-content": "oklch(85.589% 0.007 260.03)", "--color-info": "oklch(68.455% 0.148 237.251)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(78.452% 0.132 181.911)", "--color-success-content": "oklch(15.69% 0.026 181.911)", "--color-warning": "oklch(83.242% 0.139 82.95)", "--color-warning-content": "oklch(16.648% 0.027 82.95)", "--color-error": "oklch(71.785% 0.17 13.118)", "--color-error-content": "oklch(14.357% 0.034 13.118)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, caramellatte: { "color-scheme": "light", "--color-base-100": "oklch(98% 0.016 73.684)", "--color-base-200": "oklch(95% 0.038 75.164)", "--color-base-300": "oklch(90% 0.076 70.697)", "--color-base-content": "oklch(40% 0.123 38.172)", "--color-primary": "oklch(0% 0 0)", "--color-primary-content": "oklch(100% 0 0)", "--color-secondary": "oklch(22.45% 0.075 37.85)", "--color-secondary-content": "oklch(90% 0.076 70.697)", "--color-accent": "oklch(46.44% 0.111 37.85)", "--color-accent-content": "oklch(90% 0.076 70.697)", "--color-neutral": "oklch(55% 0.195 38.402)", "--color-neutral-content": "oklch(98% 0.016 73.684)", "--color-info": "oklch(42% 0.199 265.638)", "--color-info-content": "oklch(90% 0.076 70.697)", "--color-success": "oklch(43% 0.095 166.913)", "--color-success-content": "oklch(90% 0.076 70.697)", "--color-warning": "oklch(82% 0.189 84.429)", "--color-warning-content": "oklch(41% 0.112 45.904)", "--color-error": "oklch(70% 0.191 22.216)", "--color-error-content": "oklch(39% 0.141 25.723)", "--radius-selector": "2rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "1", "--noise": "1" }, autumn: { "color-scheme": "light", "--color-base-100": "oklch(95.814% 0 0)", "--color-base-200": "oklch(89.107% 0 0)", "--color-base-300": "oklch(82.4% 0 0)", "--color-base-content": "oklch(19.162% 0 0)", "--color-primary": "oklch(40.723% 0.161 17.53)", "--color-primary-content": "oklch(88.144% 0.032 17.53)", "--color-secondary": "oklch(61.676% 0.169 23.865)", "--color-secondary-content": "oklch(12.335% 0.033 23.865)", "--color-accent": "oklch(73.425% 0.094 60.729)", "--color-accent-content": "oklch(14.685% 0.018 60.729)", "--color-neutral": "oklch(54.367% 0.037 51.902)", "--color-neutral-content": "oklch(90.873% 0.007 51.902)", "--color-info": "oklch(69.224% 0.097 207.284)", "--color-info-content": "oklch(13.844% 0.019 207.284)", "--color-success": "oklch(60.995% 0.08 174.616)", "--color-success-content": "oklch(12.199% 0.016 174.616)", "--color-warning": "oklch(70.081% 0.164 56.844)", "--color-warning-content": "oklch(14.016% 0.032 56.844)", "--color-error": "oklch(53.07% 0.241 24.16)", "--color-error-content": "oklch(90.614% 0.048 24.16)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, emerald: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(93% 0 0)", "--color-base-300": "oklch(86% 0 0)", "--color-base-content": "oklch(35.519% 0.032 262.988)", "--color-primary": "oklch(76.662% 0.135 153.45)", "--color-primary-content": "oklch(33.387% 0.04 162.24)", "--color-secondary": "oklch(61.302% 0.202 261.294)", "--color-secondary-content": "oklch(100% 0 0)", "--color-accent": "oklch(72.772% 0.149 33.2)", "--color-accent-content": "oklch(0% 0 0)", "--color-neutral": "oklch(35.519% 0.032 262.988)", "--color-neutral-content": "oklch(98.462% 0.001 247.838)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, cupcake: { "color-scheme": "light", "--color-base-100": "oklch(97.788% 0.004 56.375)", "--color-base-200": "oklch(93.982% 0.007 61.449)", "--color-base-300": "oklch(91.586% 0.006 53.44)", "--color-base-content": "oklch(23.574% 0.066 313.189)", "--color-primary": "oklch(85% 0.138 181.071)", "--color-primary-content": "oklch(43% 0.078 188.216)", "--color-secondary": "oklch(89% 0.061 343.231)", "--color-secondary-content": "oklch(45% 0.187 3.815)", "--color-accent": "oklch(90% 0.076 70.697)", "--color-accent-content": "oklch(47% 0.157 37.304)", "--color-neutral": "oklch(27% 0.006 286.033)", "--color-neutral-content": "oklch(92% 0.004 286.32)", "--color-info": "oklch(68% 0.169 237.323)", "--color-info-content": "oklch(29% 0.066 243.157)", "--color-success": "oklch(69% 0.17 162.48)", "--color-success-content": "oklch(26% 0.051 172.552)", "--color-warning": "oklch(79% 0.184 86.047)", "--color-warning-content": "oklch(28% 0.066 53.813)", "--color-error": "oklch(64% 0.246 16.439)", "--color-error-content": "oklch(27% 0.105 12.094)", "--radius-selector": "1rem", "--radius-field": "2rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "2px", "--depth": "1", "--noise": "0" }, cmyk: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(95% 0 0)", "--color-base-300": "oklch(90% 0 0)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(71.772% 0.133 239.443)", "--color-primary-content": "oklch(14.354% 0.026 239.443)", "--color-secondary": "oklch(64.476% 0.202 359.339)", "--color-secondary-content": "oklch(12.895% 0.04 359.339)", "--color-accent": "oklch(94.228% 0.189 105.306)", "--color-accent-content": "oklch(18.845% 0.037 105.306)", "--color-neutral": "oklch(21.778% 0 0)", "--color-neutral-content": "oklch(84.355% 0 0)", "--color-info": "oklch(68.475% 0.094 217.284)", "--color-info-content": "oklch(13.695% 0.018 217.284)", "--color-success": "oklch(46.949% 0.162 321.406)", "--color-success-content": "oklch(89.389% 0.032 321.406)", "--color-warning": "oklch(71.236% 0.159 52.023)", "--color-warning-content": "oklch(14.247% 0.031 52.023)", "--color-error": "oklch(62.013% 0.208 28.717)", "--color-error-content": "oklch(12.402% 0.041 28.717)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, business: { "color-scheme": "dark", "--color-base-100": "oklch(24.353% 0 0)", "--color-base-200": "oklch(22.648% 0 0)", "--color-base-300": "oklch(20.944% 0 0)", "--color-base-content": "oklch(84.87% 0 0)", "--color-primary": "oklch(41.703% 0.099 251.473)", "--color-primary-content": "oklch(88.34% 0.019 251.473)", "--color-secondary": "oklch(64.092% 0.027 229.389)", "--color-secondary-content": "oklch(12.818% 0.005 229.389)", "--color-accent": "oklch(67.271% 0.167 35.791)", "--color-accent-content": "oklch(13.454% 0.033 35.791)", "--color-neutral": "oklch(27.441% 0.013 253.041)", "--color-neutral-content": "oklch(85.488% 0.002 253.041)", "--color-info": "oklch(62.616% 0.143 240.033)", "--color-info-content": "oklch(12.523% 0.028 240.033)", "--color-success": "oklch(70.226% 0.094 156.596)", "--color-success-content": "oklch(14.045% 0.018 156.596)", "--color-warning": "oklch(77.482% 0.115 81.519)", "--color-warning-content": "oklch(15.496% 0.023 81.519)", "--color-error": "oklch(51.61% 0.146 29.674)", "--color-error-content": "oklch(90.322% 0.029 29.674)", "--radius-selector": "0rem", "--radius-field": "0.25rem", "--radius-box": "0.25rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, winter: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97.466% 0.011 259.822)", "--color-base-300": "oklch(93.268% 0.016 262.751)", "--color-base-content": "oklch(41.886% 0.053 255.824)", "--color-primary": "oklch(56.86% 0.255 257.57)", "--color-primary-content": "oklch(91.372% 0.051 257.57)", "--color-secondary": "oklch(42.551% 0.161 282.339)", "--color-secondary-content": "oklch(88.51% 0.032 282.339)", "--color-accent": "oklch(59.939% 0.191 335.171)", "--color-accent-content": "oklch(11.988% 0.038 335.171)", "--color-neutral": "oklch(19.616% 0.063 257.651)", "--color-neutral-content": "oklch(83.923% 0.012 257.651)", "--color-info": "oklch(88.127% 0.085 214.515)", "--color-info-content": "oklch(17.625% 0.017 214.515)", "--color-success": "oklch(80.494% 0.077 197.823)", "--color-success-content": "oklch(16.098% 0.015 197.823)", "--color-warning": "oklch(89.172% 0.045 71.47)", "--color-warning-content": "oklch(17.834% 0.009 71.47)", "--color-error": "oklch(73.092% 0.11 20.076)", "--color-error-content": "oklch(14.618% 0.022 20.076)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" }, halloween: { "color-scheme": "dark", "--color-base-100": "oklch(21% 0.006 56.043)", "--color-base-200": "oklch(14% 0.004 49.25)", "--color-base-300": "oklch(0% 0 0)", "--color-base-content": "oklch(84.955% 0 0)", "--color-primary": "oklch(77.48% 0.204 60.62)", "--color-primary-content": "oklch(19.693% 0.004 196.779)", "--color-secondary": "oklch(45.98% 0.248 305.03)", "--color-secondary-content": "oklch(89.196% 0.049 305.03)", "--color-accent": "oklch(64.8% 0.223 136.073)", "--color-accent-content": "oklch(0% 0 0)", "--color-neutral": "oklch(24.371% 0.046 65.681)", "--color-neutral-content": "oklch(84.874% 0.009 65.681)", "--color-info": "oklch(54.615% 0.215 262.88)", "--color-info-content": "oklch(90.923% 0.043 262.88)", "--color-success": "oklch(62.705% 0.169 149.213)", "--color-success-content": "oklch(12.541% 0.033 149.213)", "--color-warning": "oklch(66.584% 0.157 58.318)", "--color-warning-content": "oklch(13.316% 0.031 58.318)", "--color-error": "oklch(65.72% 0.199 27.33)", "--color-error-content": "oklch(13.144% 0.039 27.33)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, fantasy: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(93% 0 0)", "--color-base-300": "oklch(86% 0 0)", "--color-base-content": "oklch(27.807% 0.029 256.847)", "--color-primary": "oklch(37.45% 0.189 325.02)", "--color-primary-content": "oklch(87.49% 0.037 325.02)", "--color-secondary": "oklch(53.92% 0.162 241.36)", "--color-secondary-content": "oklch(90.784% 0.032 241.36)", "--color-accent": "oklch(75.98% 0.204 56.72)", "--color-accent-content": "oklch(15.196% 0.04 56.72)", "--color-neutral": "oklch(27.807% 0.029 256.847)", "--color-neutral-content": "oklch(85.561% 0.005 256.847)", "--color-info": "oklch(72.06% 0.191 231.6)", "--color-info-content": "oklch(0% 0 0)", "--color-success": "oklch(64.8% 0.15 160)", "--color-success-content": "oklch(0% 0 0)", "--color-warning": "oklch(84.71% 0.199 83.87)", "--color-warning-content": "oklch(0% 0 0)", "--color-error": "oklch(71.76% 0.221 22.18)", "--color-error-content": "oklch(0% 0 0)", "--radius-selector": "1rem", "--radius-field": "0.5rem", "--radius-box": "1rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "1", "--noise": "0" }, wireframe: { "color-scheme": "light", "--color-base-100": "oklch(100% 0 0)", "--color-base-200": "oklch(97% 0 0)", "--color-base-300": "oklch(94% 0 0)", "--color-base-content": "oklch(20% 0 0)", "--color-primary": "oklch(87% 0 0)", "--color-primary-content": "oklch(26% 0 0)", "--color-secondary": "oklch(87% 0 0)", "--color-secondary-content": "oklch(26% 0 0)", "--color-accent": "oklch(87% 0 0)", "--color-accent-content": "oklch(26% 0 0)", "--color-neutral": "oklch(87% 0 0)", "--color-neutral-content": "oklch(26% 0 0)", "--color-info": "oklch(44% 0.11 240.79)", "--color-info-content": "oklch(90% 0.058 230.902)", "--color-success": "oklch(43% 0.095 166.913)", "--color-success-content": "oklch(90% 0.093 164.15)", "--color-warning": "oklch(47% 0.137 46.201)", "--color-warning-content": "oklch(92% 0.12 95.746)", "--color-error": "oklch(44% 0.177 26.899)", "--color-error-content": "oklch(88% 0.062 18.334)", "--radius-selector": "0rem", "--radius-field": "0.25rem", "--radius-box": "0.25rem", "--size-selector": "0.25rem", "--size-field": "0.25rem", "--border": "1px", "--depth": "0", "--noise": "0" } };
+
+// packages/daisyui/base/rootscrolllock/object.js
+var object_default2 = { ':root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not([class*="drawer-open"]) > .drawer-toggle:checked )': { overflow: "hidden" } };
+
+// packages/daisyui/functions/addPrefix.js
+var defaultExcludedPrefixes = ["color-", "size-", "radius-", "border", "depth", "noise"];
+var shouldExcludeVariable = (variableName, excludedPrefixes) => {
+ if (variableName.startsWith("tw")) {
+ return true;
+ }
+ return excludedPrefixes.some((excludedPrefix) => variableName.startsWith(excludedPrefix));
+};
+var prefixVariable = (variableName, prefix, excludedPrefixes) => {
+ if (shouldExcludeVariable(variableName, excludedPrefixes)) {
+ return variableName;
+ }
+ return `${prefix}${variableName}`;
+};
+var getPrefixedSelector = (selector, prefix) => {
+ if (!selector.startsWith("."))
+ return selector;
+ return `.${prefix}${selector.slice(1)}`;
+};
+var getPrefixedKey = (key, prefix, excludedPrefixes) => {
+ const prefixAmpDot = prefix ? `&.${prefix}` : "";
+ if (!prefix)
+ return key;
+ if (key.startsWith("--")) {
+ const variableName = key.slice(2);
+ return `--${prefixVariable(variableName, prefix, excludedPrefixes)}`;
+ }
+ if (key.startsWith("@") || key.startsWith("[")) {
+ return key;
+ }
+ if (key.startsWith("&")) {
+ if (key.match(/:[a-z-]+\(/)) {
+ return key.replace(/\.([\w-]+)/g, `.${prefix}$1`);
+ }
+ if (key.startsWith("&.")) {
+ return `${prefixAmpDot}${key.slice(2)}`;
+ }
+ return key.replace(/\.([\w-]+)/g, `.${prefix}$1`);
+ }
+ if (key.startsWith(":")) {
+ return key.replace(/\.([\w-]+)/g, `.${prefix}$1`);
+ }
+ if (key.includes(".") && !key.includes(" ") && !key.includes(">") && !key.includes("+") && !key.includes("~")) {
+ return key.split(".").filter(Boolean).map((part) => prefix + part).join(".").replace(/^/, ".");
+ }
+ if (key.includes(">") || key.includes("+") || key.includes("~")) {
+ if (key.includes(",")) {
+ return key.split(/\s*,\s*/).map((part) => {
+ return part.replace(/\.([\w-]+)/g, `.${prefix}$1`);
+ }).join(", ");
+ }
+ let processedKey = key.replace(/\.([\w-]+)/g, `.${prefix}$1`);
+ if (processedKey.startsWith(">") || processedKey.startsWith("+") || processedKey.startsWith("~")) {
+ processedKey = ` ${processedKey}`;
+ }
+ return processedKey;
+ }
+ if (key.includes(" ")) {
+ return key.split(/\s+/).map((part) => {
+ if (part.startsWith(".")) {
+ return getPrefixedSelector(part, prefix);
+ }
+ return part;
+ }).join(" ");
+ }
+ if (key.includes(":")) {
+ const [selector, ...pseudo] = key.split(":");
+ if (selector.startsWith(".")) {
+ return `${getPrefixedSelector(selector, prefix)}:${pseudo.join(":")}`;
+ }
+ return key.replace(/\.([\w-]+)/g, `.${prefix}$1`);
+ }
+ if (key.startsWith(".")) {
+ return getPrefixedSelector(key, prefix);
+ }
+ return key;
+};
+var processArrayValue = (value, prefix, excludedPrefixes) => {
+ return value.map((item) => {
+ if (typeof item === "string") {
+ if (item.startsWith(".")) {
+ return prefix ? `.${prefix}${item.slice(1)}` : item;
+ }
+ return processStringValue(item, prefix, excludedPrefixes);
+ }
+ return item;
+ });
+};
+var processStringValue = (value, prefix, excludedPrefixes) => {
+ if (prefix === 0)
+ return value;
+ return value.replace(/var\(--([^)]+)\)/g, (match, variableName) => {
+ if (shouldExcludeVariable(variableName, excludedPrefixes)) {
+ return match;
+ }
+ return `var(--${prefix}${variableName})`;
+ });
+};
+var processValue = (value, prefix, excludedPrefixes) => {
+ if (Array.isArray(value)) {
+ return processArrayValue(value, prefix, excludedPrefixes);
+ } else if (typeof value === "object" && value !== null) {
+ return addPrefix(value, prefix, excludedPrefixes);
+ } else if (typeof value === "string") {
+ return processStringValue(value, prefix, excludedPrefixes);
+ } else {
+ return value;
+ }
+};
+var addPrefix = (obj, prefix, excludedPrefixes = defaultExcludedPrefixes) => {
+ return Object.entries(obj).reduce((result, [key, value]) => {
+ const newKey = getPrefixedKey(key, prefix, excludedPrefixes);
+ result[newKey] = processValue(value, prefix, excludedPrefixes);
+ return result;
+ }, {});
+};
+
+// packages/daisyui/base/rootscrolllock/index.js
+var rootscrolllock_default = ({ addBase, prefix = "" }) => {
+ const prefixedrootscrolllock = addPrefix(object_default2, prefix);
+ addBase({ ...prefixedrootscrolllock });
+};
+
+// packages/daisyui/base/rootcolor/object.js
+var object_default3 = { ":root, [data-theme]": { "background-color": "var(--root-bg, var(--color-base-100))", color: "var(--color-base-content)" } };
+
+// packages/daisyui/base/rootcolor/index.js
+var rootcolor_default = ({ addBase, prefix = "" }) => {
+ const prefixedrootcolor = addPrefix(object_default3, prefix);
+ addBase({ ...prefixedrootcolor });
+};
+
+// packages/daisyui/base/scrollbar/object.js
+var object_default4 = { ":root": { "scrollbar-color": "color-mix(in oklch, currentColor 35%, #0000) #0000" } };
+
+// packages/daisyui/base/scrollbar/index.js
+var scrollbar_default = ({ addBase, prefix = "" }) => {
+ const prefixedscrollbar = addPrefix(object_default4, prefix);
+ addBase({ ...prefixedscrollbar });
+};
+
+// packages/daisyui/base/properties/object.js
+var object_default5 = { "@property --radialprogress": { syntax: '"<percentage>"', inherits: "true", "initial-value": "0%" } };
+
+// packages/daisyui/base/properties/index.js
+var properties_default = ({ addBase, prefix = "" }) => {
+ const prefixedproperties = addPrefix(object_default5, prefix);
+ addBase({ ...prefixedproperties });
+};
+
+// packages/daisyui/base/rootscrollgutter/object.js
+var object_default6 = { ":where( :root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not(.drawer-open) > .drawer-toggle:checked ) )": { "scrollbar-gutter": "stable", "background-image": "linear-gradient(var(--color-base-100), var(--color-base-100))", "--root-bg": "color-mix(in srgb, var(--color-base-100), oklch(0% 0 0) 40%)" }, ":where(.modal[open], .modal-open, .modal-toggle:checked + .modal):not(.modal-start, .modal-end)": { "scrollbar-gutter": "stable" } };
+
+// packages/daisyui/base/rootscrollgutter/index.js
+var rootscrollgutter_default = ({ addBase, prefix = "" }) => {
+ const prefixedrootscrollgutter = addPrefix(object_default6, prefix);
+ addBase({ ...prefixedrootscrollgutter });
+};
+
+// packages/daisyui/base/svg/object.js
+var object_default7 = { ":root": { "--fx-noise": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E")` }, ".chat": { "--mask-chat": `url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e")` } };
+
+// packages/daisyui/base/svg/index.js
+var svg_default = ({ addBase, prefix = "" }) => {
+ const prefixedsvg = addPrefix(object_default7, prefix);
+ addBase({ ...prefixedsvg });
+};
+
+// packages/daisyui/components/drawer/object.js
+var object_default8 = { ".drawer": { position: "relative", display: "grid", width: "100%", "grid-auto-columns": "max-content auto" }, ".drawer-content": { "grid-column-start": "2", "grid-row-start": "1", "min-width": "calc(0.25rem * 0)" }, ".drawer-side": { "pointer-events": "none", visibility: "hidden", position: "fixed", "inset-inline-start": "calc(0.25rem * 0)", top: "calc(0.25rem * 0)", "z-index": 1, "grid-column-start": "1", "grid-row-start": "1", display: "grid", width: "100%", "grid-template-columns": "repeat(1, minmax(0, 1fr))", "grid-template-rows": "repeat(1, minmax(0, 1fr))", "align-items": "flex-start", "justify-items": "start", "overflow-x": "hidden", "overflow-y": "hidden", "overscroll-behavior": "contain", opacity: "0%", transition: "opacity 0.2s ease-out 0.1s allow-discrete, visibility 0.3s ease-out 0.1s allow-discrete", height: ["100vh", "100dvh"], "> .drawer-overlay": { position: "sticky", top: "calc(0.25rem * 0)", cursor: "pointer", "place-self": "stretch", "background-color": "oklch(0% 0 0 / 40%)" }, "> *": { "grid-column-start": "1", "grid-row-start": "1" }, "> *:not(.drawer-overlay)": { "will-change": "transform", transition: "translate 0.3s ease-out", translate: "-100%", '[dir="rtl"] &': { translate: "100%" } } }, ".drawer-toggle": { position: "fixed", height: "calc(0.25rem * 0)", width: "calc(0.25rem * 0)", appearance: "none", opacity: "0%", "&:checked": { "& ~ .drawer-side": { "pointer-events": "auto", visibility: "visible", "overflow-y": "auto", opacity: "100%", "& > *:not(.drawer-overlay)": { translate: "0%" } } }, "&:focus-visible ~ .drawer-content label.drawer-button": { outline: "2px solid", "outline-offset": "2px" } }, ".drawer-end": { "grid-auto-columns": "auto max-content", "> .drawer-toggle": { "& ~ .drawer-content": { "grid-column-start": "1" }, "& ~ .drawer-side": { "grid-column-start": "2", "justify-items": "end" }, "& ~ .drawer-side > *:not(.drawer-overlay)": { translate: "100%", '[dir="rtl"] &': { translate: "-100%" } }, "&:checked ~ .drawer-side > *:not(.drawer-overlay)": { translate: "0%" } } }, ".drawer-open": { "> .drawer-side": { "overflow-y": "auto" }, "> .drawer-toggle": { display: "none", "& ~ .drawer-side": { "pointer-events": "auto", visibility: "visible", position: "sticky", display: "block", width: "auto", "overscroll-behavior": "auto", opacity: "100%", "& > .drawer-overlay": { cursor: "default", "background-color": "transparent" }, "& > *:not(.drawer-overlay)": { translate: "0%", '[dir="rtl"] &': { translate: "0%" } } }, "&:checked ~ .drawer-side": { "pointer-events": "auto", visibility: "visible" } } } };
+
+// packages/daisyui/components/drawer/index.js
+var drawer_default = ({ addComponents, prefix = "" }) => {
+ const prefixeddrawer = addPrefix(object_default8, prefix);
+ addComponents({ ...prefixeddrawer });
+};
+
+// packages/daisyui/components/link/object.js
+var object_default9 = { ".link": { cursor: "pointer", "text-decoration-line": "underline", "&:focus": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, "&:focus-visible": { outline: "2px solid currentColor", "outline-offset": "2px" } }, ".link-hover": { "text-decoration-line": "none", "&:hover": { "@media (hover: hover)": { "text-decoration-line": "underline" } } }, ".link-primary": { color: "var(--color-primary)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-primary) 80%, #000)" } } }, ".link-secondary": { color: "var(--color-secondary)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-secondary) 80%, #000)" } } }, ".link-accent": { color: "var(--color-accent)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-accent) 80%, #000)" } } }, ".link-neutral": { color: "var(--color-neutral)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-neutral) 80%, #000)" } } }, ".link-success": { color: "var(--color-success)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-success) 80%, #000)" } } }, ".link-info": { color: "var(--color-info)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-info) 80%, #000)" } } }, ".link-warning": { color: "var(--color-warning)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-warning) 80%, #000)" } } }, ".link-error": { color: "var(--color-error)", "@media (hover: hover)": { "&:hover": { color: "color-mix(in oklab, var(--color-error) 80%, #000)" } } } };
+
+// packages/daisyui/components/link/index.js
+var link_default = ({ addComponents, prefix = "" }) => {
+ const prefixedlink = addPrefix(object_default9, prefix);
+ addComponents({ ...prefixedlink });
+};
+
+// packages/daisyui/components/stat/object.js
+var object_default10 = { ".stats": { position: "relative", display: "inline-grid", "grid-auto-flow": "column", "overflow-x": "auto", "border-radius": "var(--radius-box)" }, ".stat": { display: "inline-grid", width: "100%", "column-gap": "calc(0.25rem * 4)", "padding-inline": "calc(0.25rem * 6)", "padding-block": "calc(0.25rem * 4)", "grid-template-columns": "repeat(1, 1fr)", "&:not(:last-child)": { "border-inline-end": "var(--border) dashed color-mix(in oklab, currentColor 10%, #0000)", "border-block-end": "none" } }, ".stat-figure": { "grid-column-start": "2", "grid-row": "span 3 / span 3", "grid-row-start": "1", "place-self": "center", "justify-self": "flex-end" }, ".stat-title": { "grid-column-start": "1", "white-space": "nowrap", color: "color-mix(in oklab, var(--color-base-content) 60%, transparent)", "font-size": "0.75rem" }, ".stat-value": { "grid-column-start": "1", "white-space": "nowrap", "font-size": "2rem", "font-weight": 800 }, ".stat-desc": { "grid-column-start": "1", "white-space": "nowrap", color: "color-mix(in oklab, var(--color-base-content) 60%, transparent)", "font-size": "0.75rem" }, ".stat-actions": { "grid-column-start": "1", "white-space": "nowrap" }, ".stats-horizontal": { "grid-auto-flow": "column", "overflow-x": "auto", ".stat:not(:last-child)": { "border-inline-end": "var(--border) dashed color-mix(in oklab, currentColor 10%, #0000)", "border-block-end": "none" } }, ".stats-vertical": { "grid-auto-flow": "row", "overflow-y": "auto", ".stat:not(:last-child)": { "border-inline-end": "none", "border-block-end": "var(--border) dashed color-mix(in oklab, currentColor 10%, #0000)" } } };
+
+// packages/daisyui/components/stat/index.js
+var stat_default = ({ addComponents, prefix = "" }) => {
+ const prefixedstat = addPrefix(object_default10, prefix);
+ addComponents({ ...prefixedstat });
+};
+
+// packages/daisyui/components/carousel/object.js
+var object_default11 = { ".carousel": { display: "inline-flex", "overflow-x": "scroll", "scroll-snap-type": "x mandatory", "scroll-behavior": "smooth", "scrollbar-width": "none", "&::-webkit-scrollbar": { display: "none" } }, ".carousel-vertical": { "flex-direction": "column", "overflow-y": "scroll", "scroll-snap-type": "y mandatory" }, ".carousel-horizontal": { "flex-direction": "row", "overflow-x": "scroll", "scroll-snap-type": "x mandatory" }, ".carousel-item": { "box-sizing": "content-box", display: "flex", flex: "none", "scroll-snap-align": "start" }, ".carousel-start": { ".carousel-item": { "scroll-snap-align": "start" } }, ".carousel-center": { ".carousel-item": { "scroll-snap-align": "center" } }, ".carousel-end": { ".carousel-item": { "scroll-snap-align": "end" } } };
+
+// packages/daisyui/components/carousel/index.js
+var carousel_default = ({ addComponents, prefix = "" }) => {
+ const prefixedcarousel = addPrefix(object_default11, prefix);
+ addComponents({ ...prefixedcarousel });
+};
+
+// packages/daisyui/components/divider/object.js
+var object_default12 = { ".divider": { display: "flex", height: "calc(0.25rem * 4)", "flex-direction": "row", "align-items": "center", "align-self": "stretch", "white-space": "nowrap", margin: "var(--divider-m, 1rem 0)", "--divider-color": "color-mix(in oklab, var(--color-base-content) 10%, transparent)", "&:before, &:after": { content: '""', height: "calc(0.25rem * 0.5)", width: "100%", "flex-grow": 1, "background-color": "var(--divider-color)" }, "@media print": { "&:before, &:after": { border: "0.5px solid" } }, "&:not(:empty)": { gap: "calc(0.25rem * 4)" } }, ".divider-horizontal": { "--divider-m": "0 1rem", "&.divider": { height: "auto", width: "calc(0.25rem * 4)", "flex-direction": "column", "&:before": { height: "100%", width: "calc(0.25rem * 0.5)" }, "&:after": { height: "100%", width: "calc(0.25rem * 0.5)" } } }, ".divider-vertical": { "--divider-m": "1rem 0", "&.divider": { height: "calc(0.25rem * 4)", width: "auto", "flex-direction": "row", "&:before": { height: "calc(0.25rem * 0.5)", width: "100%" }, "&:after": { height: "calc(0.25rem * 0.5)", width: "100%" } } }, ".divider-neutral": { "&:before, &:after": { "background-color": "var(--color-neutral)" } }, ".divider-primary": { "&:before, &:after": { "background-color": "var(--color-primary)" } }, ".divider-secondary": { "&:before, &:after": { "background-color": "var(--color-secondary)" } }, ".divider-accent": { "&:before, &:after": { "background-color": "var(--color-accent)" } }, ".divider-success": { "&:before, &:after": { "background-color": "var(--color-success)" } }, ".divider-warning": { "&:before, &:after": { "background-color": "var(--color-warning)" } }, ".divider-info": { "&:before, &:after": { "background-color": "var(--color-info)" } }, ".divider-error": { "&:before, &:after": { "background-color": "var(--color-error)" } }, ".divider-start:before": { display: "none" }, ".divider-end:after": { display: "none" } };
+
+// packages/daisyui/components/divider/index.js
+var divider_default = ({ addComponents, prefix = "" }) => {
+ const prefixeddivider = addPrefix(object_default12, prefix);
+ addComponents({ ...prefixeddivider });
+};
+
+// packages/daisyui/components/mask/object.js
+var object_default13 = { ".mask": { display: "inline-block", "vertical-align": "middle", "mask-size": "contain", "mask-repeat": "no-repeat", "mask-position": "center" }, ".mask-half-1": { "mask-size": "200%", "mask-position": ["left", "left"], '&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)': { "mask-position": "right" } }, ".mask-half-2": { "mask-size": "200%", "mask-position": ["right", "right"], '&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)': { "mask-position": "left" } }, ".mask-squircle": { "mask-image": `url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 0C20 0 0 20 0 100s20 100 100 100 100-20 100-100S180 0 100 0Z'/%3e%3c/svg%3e")` }, ".mask-decagon": { "mask-image": `url("data:image/svg+xml,%3csvg width='192' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 0 58.779 19.098 36.327 50v61.804l-36.327 50L96 200l-58.779-19.098-36.327-50V69.098l36.327-50z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-diamond": { "mask-image": `url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m100 0 100 100-100 100L0 100z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-heart": { "mask-image": `url("data:image/svg+xml,%3csvg width='200' height='185' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 184.606a15.384 15.384 0 0 1-8.653-2.678C53.565 156.28 37.205 138.695 28.182 127.7 8.952 104.264-.254 80.202.005 54.146.308 24.287 24.264 0 53.406 0c21.192 0 35.869 11.937 44.416 21.879a2.884 2.884 0 0 0 4.356 0C110.725 11.927 125.402 0 146.594 0c29.142 0 53.098 24.287 53.4 54.151.26 26.061-8.956 50.122-28.176 73.554-9.023 10.994-25.383 28.58-63.165 54.228a15.384 15.384 0 0 1-8.653 2.673Z' fill='black' fill-rule='nonzero'/%3e%3c/svg%3e")` }, ".mask-hexagon": { "mask-image": `url("data:image/svg+xml,%3csvg width='182' height='201' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M.3 65.486c0-9.196 6.687-20.063 14.211-25.078l61.86-35.946c8.36-5.016 20.899-5.016 29.258 0l61.86 35.946c8.36 5.015 14.211 15.882 14.211 25.078v71.055c0 9.196-6.687 20.063-14.211 25.079l-61.86 35.945c-8.36 4.18-20.899 4.18-29.258 0L14.51 161.62C6.151 157.44.3 145.737.3 136.54V65.486Z' fill='black' fill-rule='nonzero'/%3e%3c/svg%3e")` }, ".mask-hexagon-2": { "mask-image": `url("data:image/svg+xml,%3csvg width='200' height='182' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M64.786 181.4c-9.196 0-20.063-6.687-25.079-14.21L3.762 105.33c-5.016-8.36-5.016-20.9 0-29.259l35.945-61.86C44.723 5.851 55.59 0 64.786 0h71.055c9.196 0 20.063 6.688 25.079 14.211l35.945 61.86c4.18 8.36 4.18 20.899 0 29.258l-35.945 61.86c-4.18 8.36-15.883 14.211-25.079 14.211H64.786Z' fill='black' fill-rule='nonzero'/%3e%3c/svg%3e")` }, ".mask-circle": { "mask-image": `url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle fill='black' cx='100' cy='100' r='100' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-pentagon": { "mask-image": `url("data:image/svg+xml,%3csvg width='192' height='181' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 0 95.106 69.098-36.327 111.804H37.22L.894 69.098z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-star": { "mask-image": `url("data:image/svg+xml,%3csvg width='192' height='180' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 137.263-58.779 42.024 22.163-68.389L.894 68.481l72.476-.243L96 0l22.63 68.238 72.476.243-58.49 42.417 22.163 68.389z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-star-2": { "mask-image": `url("data:image/svg+xml,%3csvg width='192' height='180' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 153.044-58.779 26.243 7.02-63.513L.894 68.481l63.117-13.01L96 0l31.989 55.472 63.117 13.01-43.347 47.292 7.02 63.513z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-triangle": { "mask-image": `url("data:image/svg+xml,%3csvg width='174' height='149' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m87 148.476-86.603.185L43.86 74.423 87 0l43.14 74.423 43.463 74.238z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-triangle-2": { "mask-image": `url("data:image/svg+xml,%3csvg width='174' height='150' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m87 .738 86.603-.184-43.463 74.238L87 149.214 43.86 74.792.397.554z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-triangle-3": { "mask-image": `url("data:image/svg+xml,%3csvg width='150' height='174' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m149.369 87.107.185 86.603-74.239-43.463L.893 87.107l74.422-43.14L149.554.505z' fill-rule='evenodd'/%3e%3c/svg%3e")` }, ".mask-triangle-4": { "mask-image": `url("data:image/svg+xml,%3csvg width='150' height='174' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M.631 87.107.446.505l74.239 43.462 74.422 43.14-74.422 43.14L.446 173.71z' fill-rule='evenodd'/%3e%3c/svg%3e")` } };
+
+// packages/daisyui/components/mask/index.js
+var mask_default = ({ addComponents, prefix = "" }) => {
+ const prefixedmask = addPrefix(object_default13, prefix);
+ addComponents({ ...prefixedmask });
+};
+
+// packages/daisyui/components/fieldset/object.js
+var object_default14 = { ".fieldset": { display: "grid", gap: "calc(0.25rem * 1.5)", "padding-block": "calc(0.25rem * 1)", "font-size": "0.75rem", "grid-template-columns": "1fr", "grid-auto-rows": "max-content" }, ".fieldset-legend": { "margin-bottom": "calc(0.25rem * -1)", display: "flex", "align-items": "center", "justify-content": "space-between", gap: "calc(0.25rem * 2)", "padding-block": "calc(0.25rem * 2)", color: "var(--color-base-content)", "font-weight": 600 }, ".fieldset-label": { display: "flex", "align-items": "center", gap: "calc(0.25rem * 1.5)", color: "color-mix(in oklab, var(--color-base-content) 60%, transparent)", "&:has(input)": { cursor: "pointer" } } };
+
+// packages/daisyui/components/fieldset/index.js
+var fieldset_default = ({ addComponents, prefix = "" }) => {
+ const prefixedfieldset = addPrefix(object_default14, prefix);
+ addComponents({ ...prefixedfieldset });
+};
+
+// packages/daisyui/components/dropdown/object.js
+var object_default15 = { ".dropdown": { position: "relative", display: "inline-block", "position-area": "var(--anchor-v, bottom) var(--anchor-h, span-right)", "& > *:not(summary):focus": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, ".dropdown-content": { position: "absolute" }, "&:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within)": { ".dropdown-content": { display: "none", "transform-origin": "top", opacity: "0%", scale: "95%" } }, "&[popover], .dropdown-content": { "z-index": 999, animation: "dropdown 0.2s", "transition-property": "opacity, scale, display", "transition-behavior": "allow-discrete", "transition-duration": "0.2s", "transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)" }, "@starting-style": { "&[popover], .dropdown-content": { scale: "95%", opacity: 0 } }, "&.dropdown-open, &:not(.dropdown-hover):focus, &:focus-within": { "> [tabindex]:first-child": { "pointer-events": "none" }, ".dropdown-content": { opacity: "100%" } }, "&.dropdown-hover:hover": { ".dropdown-content": { opacity: "100%", scale: "100%" } }, "&:is(details)": { summary: { "&::-webkit-details-marker": { display: "none" } } }, "&.dropdown-open, &:focus, &:focus-within": { ".dropdown-content": { scale: "100%" } }, "&:where([popover])": { background: "#0000" }, "&[popover]": { position: "fixed", color: "inherit", "@supports not (position-area: bottom)": { margin: "auto", "&.dropdown-open:not(:popover-open)": { display: "none", "transform-origin": "top", opacity: "0%", scale: "95%" }, "&::backdrop": { "background-color": "color-mix(in oklab, #000 30%, #0000)" } }, "&:not(.dropdown-open, :popover-open)": { display: "none", "transform-origin": "top", opacity: "0%", scale: "95%" } } }, ".dropdown-start": { "--anchor-h": "span-right", ":where(.dropdown-content)": { "inset-inline-end": "auto" }, "&.dropdown-left": { "--anchor-h": "left", "--anchor-v": "span-bottom", ".dropdown-content": { top: "calc(0.25rem * 0)", bottom: "auto" } }, "&.dropdown-right": { "--anchor-h": "right", "--anchor-v": "span-bottom", ".dropdown-content": { top: "calc(0.25rem * 0)", bottom: "auto" } } }, ".dropdown-center": { "--anchor-h": "center", ":where(.dropdown-content)": { "inset-inline-end": "calc(1/2 * 100%)", translate: "50% 0", '[dir="rtl"] &': { translate: "-50% 0" } }, "&.dropdown-left": { "--anchor-h": "left", "--anchor-v": "center", ".dropdown-content": { top: "auto", bottom: "calc(1/2 * 100%)", translate: "0 50%" } }, "&.dropdown-right": { "--anchor-h": "right", "--anchor-v": "center", ".dropdown-content": { top: "auto", bottom: "calc(1/2 * 100%)", translate: "0 50%" } } }, ".dropdown-end": { "--anchor-h": "span-left", ":where(.dropdown-content)": { "inset-inline-end": "calc(0.25rem * 0)", translate: "0 0" }, "&.dropdown-left": { "--anchor-h": "left", "--anchor-v": "span-top", ".dropdown-content": { top: "auto", bottom: "calc(0.25rem * 0)" } }, "&.dropdown-right": { "--anchor-h": "right", "--anchor-v": "span-top", ".dropdown-content": { top: "auto", bottom: "calc(0.25rem * 0)" } } }, ".dropdown-left": { "--anchor-h": "left", "--anchor-v": "span-bottom", ".dropdown-content": { "inset-inline-end": "100%", top: "calc(0.25rem * 0)", bottom: "auto", "transform-origin": "right" } }, ".dropdown-right": { "--anchor-h": "right", "--anchor-v": "span-bottom", ".dropdown-content": { "inset-inline-start": "100%", top: "calc(0.25rem * 0)", bottom: "auto", "transform-origin": "left" } }, ".dropdown-bottom": { "--anchor-v": "bottom", ".dropdown-content": { top: "100%", bottom: "auto", "transform-origin": "top" } }, ".dropdown-top": { "--anchor-v": "top", ".dropdown-content": { top: "auto", bottom: "100%", "transform-origin": "bottom" } }, "@keyframes dropdown": { "0%": { opacity: 0 } } };
+
+// packages/daisyui/components/dropdown/index.js
+var dropdown_default = ({ addComponents, prefix = "" }) => {
+ const prefixeddropdown = addPrefix(object_default15, prefix);
+ addComponents({ ...prefixeddropdown });
+};
+
+// packages/daisyui/components/card/object.js
+var object_default16 = { ".card": { position: "relative", display: "flex", "flex-direction": "column", "border-radius": "var(--radius-box)", "outline-width": "2px", transition: "outline 0.2s ease-in-out", outline: "0 solid #0000", "outline-offset": "2px", "&:focus": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, "&:focus-visible": { "outline-color": "currentColor" }, ":where(figure:first-child)": { overflow: "hidden", "border-start-start-radius": "inherit", "border-start-end-radius": "inherit", "border-end-start-radius": "unset", "border-end-end-radius": "unset" }, ":where(figure:last-child)": { overflow: "hidden", "border-start-start-radius": "unset", "border-start-end-radius": "unset", "border-end-start-radius": "inherit", "border-end-end-radius": "inherit" }, "&:where(.card-border)": { border: "var(--border) solid var(--color-base-200)" }, "&:where(.card-dash)": { border: "var(--border) dashed var(--color-base-200)" }, "&.image-full": { display: "grid", "> *": { "grid-column-start": "1", "grid-row-start": "1" }, "> .card-body": { position: "relative", color: "var(--color-neutral-content)" }, ":where(figure)": { overflow: "hidden", "border-radius": "inherit" }, "> figure img": { height: "100%", "object-fit": "cover", filter: "brightness(28%)" } }, figure: { display: "flex", "align-items": "center", "justify-content": "center" }, '&:has(> input:is(input[type="checkbox"], input[type="radio"]))': { cursor: "pointer", "user-select": "none" }, "&:has(> :checked)": { outline: "2px solid currentColor" } }, ".card-title": { display: "flex", "align-items": "center", gap: "calc(0.25rem * 2)", "font-size": "var(--cardtitle-fs, 1.125rem)", "font-weight": 600 }, ".card-body": { display: "flex", flex: "auto", "flex-direction": "column", gap: "calc(0.25rem * 2)", padding: "var(--card-p, 1.5rem)", "font-size": "var(--card-fs, 0.875rem)", ":where(p)": { "flex-grow": 1 } }, ".card-actions": { display: "flex", "flex-wrap": "wrap", "align-items": "flex-start", gap: "calc(0.25rem * 2)" }, ".card-xs": { ".card-body": { "--card-p": "0.5rem", "--card-fs": "0.6875rem" }, ".card-title": { "--cardtitle-fs": "0.875rem" } }, ".card-sm": { ".card-body": { "--card-p": "1rem", "--card-fs": "0.75rem" }, ".card-title": { "--cardtitle-fs": "1rem" } }, ".card-md": { ".card-body": { "--card-p": "1.5rem", "--card-fs": "0.875rem" }, ".card-title": { "--cardtitle-fs": "1.125rem" } }, ".card-lg": { ".card-body": { "--card-p": "2rem", "--card-fs": "1rem" }, ".card-title": { "--cardtitle-fs": "1.25rem" } }, ".card-xl": { ".card-body": { "--card-p": "2.5rem", "--card-fs": "1.125rem" }, ".card-title": { "--cardtitle-fs": "1.375rem" } }, ".card-side": { "align-items": "stretch", "flex-direction": "row", ":where(figure:first-child)": { overflow: "hidden", "border-start-start-radius": "inherit", "border-start-end-radius": "unset", "border-end-start-radius": "inherit", "border-end-end-radius": "unset" }, ":where(figure:last-child)": { overflow: "hidden", "border-start-start-radius": "unset", "border-start-end-radius": "inherit", "border-end-start-radius": "unset", "border-end-end-radius": "inherit" }, "figure > *": { "max-width": "unset" }, ":where(figure > *)": { width: "100%", height: "100%", "object-fit": "cover" } } };
+
+// packages/daisyui/components/card/index.js
+var card_default = ({ addComponents, prefix = "" }) => {
+ const prefixedcard = addPrefix(object_default16, prefix);
+ addComponents({ ...prefixedcard });
+};
+
+// packages/daisyui/components/steps/object.js
+var object_default17 = { ".steps": { display: "inline-grid", "grid-auto-flow": "column", overflow: "hidden", "overflow-x": "auto", "counter-reset": "step", "grid-auto-columns": "1fr", ".step": { display: "grid", "grid-template-columns": ["repeat(1, minmax(0, 1fr))", "auto"], "grid-template-rows": ["repeat(2, minmax(0, 1fr))", "40px 1fr"], "place-items": "center", "text-align": "center", "min-width": "4rem", "--step-bg": "var(--color-base-300)", "--step-fg": "var(--color-base-content)", "&:before": { top: "calc(0.25rem * 0)", "grid-column-start": "1", "grid-row-start": "1", height: "calc(0.25rem * 2)", width: "100%", border: "1px solid", color: "var(--step-bg)", "background-color": "var(--step-bg)", "--tw-content": '""', content: "var(--tw-content)", "margin-inline-start": "-100%" }, "> .step-icon, &:not(:has(.step-icon)):after": { content: "counter(step)", "counter-increment": "step", "z-index": 1, color: "var(--step-fg)", "background-color": "var(--step-bg)", border: "1px solid var(--step-bg)", position: "relative", "grid-column-start": "1", "grid-row-start": "1", display: "grid", height: "calc(0.25rem * 8)", width: "calc(0.25rem * 8)", "place-items": "center", "place-self": "center", "border-radius": "calc(infinity * 1px)" }, "&:first-child:before": { content: "none" }, "&[data-content]:after": { content: "attr(data-content)" } }, ".step-neutral": { "+ .step-neutral:before, &:after, > .step-icon": { "--step-bg": "var(--color-neutral)", "--step-fg": "var(--color-neutral-content)" } }, ".step-primary": { "+ .step-primary:before, &:after, > .step-icon": { "--step-bg": "var(--color-primary)", "--step-fg": "var(--color-primary-content)" } }, ".step-secondary": { "+ .step-secondary:before, &:after, > .step-icon": { "--step-bg": "var(--color-secondary)", "--step-fg": "var(--color-secondary-content)" } }, ".step-accent": { "+ .step-accent:before, &:after, > .step-icon": { "--step-bg": "var(--color-accent)", "--step-fg": "var(--color-accent-content)" } }, ".step-info": { "+ .step-info:before, &:after, > .step-icon": { "--step-bg": "var(--color-info)", "--step-fg": "var(--color-info-content)" } }, ".step-success": { "+ .step-success:before, &:after, > .step-icon": { "--step-bg": "var(--color-success)", "--step-fg": "var(--color-success-content)" } }, ".step-warning": { "+ .step-warning:before, &:after, > .step-icon": { "--step-bg": "var(--color-warning)", "--step-fg": "var(--color-warning-content)" } }, ".step-error": { "+ .step-error:before, &:after, > .step-icon": { "--step-bg": "var(--color-error)", "--step-fg": "var(--color-error-content)" } } }, ".steps-horizontal": { "grid-auto-columns": "1fr", display: "inline-grid", "grid-auto-flow": "column", overflow: "hidden", "overflow-x": "auto", ".step": { display: "grid", "grid-template-columns": ["repeat(1, minmax(0, 1fr))", "auto"], "grid-template-rows": ["repeat(2, minmax(0, 1fr))", "40px 1fr"], "place-items": "center", "text-align": "center", "min-width": "4rem", "&:before": { height: "calc(0.25rem * 2)", width: "100%", translate: "0", content: '""', "margin-inline-start": "-100%" }, '[dir="rtl"] &:before': { translate: "0" } } }, ".steps-vertical": { "grid-auto-rows": "1fr", "grid-auto-flow": "row", ".step": { display: "grid", "grid-template-columns": ["repeat(2, minmax(0, 1fr))", "40px 1fr"], "grid-template-rows": ["repeat(1, minmax(0, 1fr))", "auto"], gap: "0.5rem", "min-height": "4rem", "justify-items": "start", "&:before": { height: "100%", width: "calc(0.25rem * 2)", translate: "-50% -50%", "margin-inline-start": "50%" }, '[dir="rtl"] &:before': { translate: "50% -50%" } } } };
+
+// packages/daisyui/components/steps/index.js
+var steps_default = ({ addComponents, prefix = "" }) => {
+ const prefixedsteps = addPrefix(object_default17, prefix);
+ addComponents({ ...prefixedsteps });
+};
+
+// packages/daisyui/components/alert/object.js
+var object_default18 = { ".alert": { display: "grid", "align-items": "center", gap: "calc(0.25rem * 4)", "border-radius": "var(--radius-box)", "padding-inline": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 3)", color: "var(--color-base-content)", "background-color": "var(--alert-color, var(--color-base-200))", "justify-content": "start", "justify-items": "start", "grid-auto-flow": "column", "grid-template-columns": "auto", "text-align": "start", border: "var(--border) solid var(--color-base-200)", "font-size": "0.875rem", "line-height": "1.25rem", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--fx-noise)", "box-shadow": "0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px color-mix( in oklab, color-mix(in oklab, #000 20%, var(--alert-color, var(--color-base-200))) calc(var(--depth) * 20%), #0000 ), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08))", "&:has(:nth-child(2))": { "grid-template-columns": "auto minmax(auto, 1fr)" }, "&.alert-outline": { "background-color": "transparent", color: "var(--alert-color)", "box-shadow": "none", "background-image": "none" }, "&.alert-dash": { "background-color": "transparent", color: "var(--alert-color)", "border-style": "dashed", "box-shadow": "none", "background-image": "none" }, "&.alert-soft": { color: "var(--alert-color, var(--color-base-content))", background: "color-mix( in oklab, var(--alert-color, var(--color-base-content)) 8%, var(--color-base-100) )", "border-color": "color-mix( in oklab, var(--alert-color, var(--color-base-content)) 10%, var(--color-base-100) )", "box-shadow": "none", "background-image": "none" } }, ".alert-info": { "border-color": "var(--color-info)", color: "var(--color-info-content)", "--alert-color": "var(--color-info)" }, ".alert-success": { "border-color": "var(--color-success)", color: "var(--color-success-content)", "--alert-color": "var(--color-success)" }, ".alert-warning": { "border-color": "var(--color-warning)", color: "var(--color-warning-content)", "--alert-color": "var(--color-warning)" }, ".alert-error": { "border-color": "var(--color-error)", color: "var(--color-error-content)", "--alert-color": "var(--color-error)" }, ".alert-vertical": { "justify-content": "center", "justify-items": "center", "grid-auto-flow": "row", "grid-template-columns": "auto", "text-align": "center", "&:has(:nth-child(2))": { "grid-template-columns": "auto" } }, ".alert-horizontal": { "justify-content": "start", "justify-items": "start", "grid-auto-flow": "column", "grid-template-columns": "auto", "text-align": "start", "&:has(:nth-child(2))": { "grid-template-columns": "auto minmax(auto, 1fr)" } } };
+
+// packages/daisyui/components/alert/index.js
+var alert_default = ({ addComponents, prefix = "" }) => {
+ const prefixedalert = addPrefix(object_default18, prefix);
+ addComponents({ ...prefixedalert });
+};
+
+// packages/daisyui/components/kbd/object.js
+var object_default19 = { ".kbd": { display: "inline-flex", "align-items": "center", "justify-content": "center", "border-radius": "var(--radius-field)", "background-color": "var(--color-base-200)", "vertical-align": "middle", "padding-left": "0.5em", "padding-right": "0.5em", border: "var(--border) solid color-mix(in srgb, var(--color-base-content) 20%, #0000)", "border-bottom": "calc(var(--border) + 1px) solid color-mix(in srgb, var(--color-base-content) 20%, #0000)", "--size": "calc(var(--size-selector, 0.25rem) * 6)", "font-size": "0.875rem", height: "var(--size)", "min-width": "var(--size)" }, ".kbd-xs": { "--size": "calc(var(--size-selector, 0.25rem) * 4)", "font-size": "0.625rem" }, ".kbd-sm": { "--size": "calc(var(--size-selector, 0.25rem) * 5)", "font-size": "0.75rem" }, ".kbd-md": { "--size": "calc(var(--size-selector, 0.25rem) * 6)", "font-size": "0.875rem" }, ".kbd-lg": { "--size": "calc(var(--size-selector, 0.25rem) * 7)", "font-size": "1rem" }, ".kbd-xl": { "--size": "calc(var(--size-selector, 0.25rem) * 8)", "font-size": "1.125rem" } };
+
+// packages/daisyui/components/kbd/index.js
+var kbd_default = ({ addComponents, prefix = "" }) => {
+ const prefixedkbd = addPrefix(object_default19, prefix);
+ addComponents({ ...prefixedkbd });
+};
+
+// packages/daisyui/components/select/object.js
+var object_default20 = { ".select": { border: "var(--border) solid #0000", position: "relative", display: "inline-flex", "flex-shrink": 1, appearance: "none", "align-items": "center", gap: "calc(0.25rem * 1.5)", "background-color": "var(--color-base-100)", "padding-inline-start": "calc(0.25rem * 4)", "padding-inline-end": "calc(0.25rem * 7)", "vertical-align": "middle", width: "clamp(3rem, 20rem, 100%)", height: "var(--size)", "font-size": "0.875rem", "border-start-start-radius": "var(--join-ss, var(--radius-field))", "border-start-end-radius": "var(--join-se, var(--radius-field))", "border-end-start-radius": "var(--join-es, var(--radius-field))", "border-end-end-radius": "var(--join-ee, var(--radius-field))", "background-image": "linear-gradient(45deg, #0000 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, #0000 50%)", "background-position": "calc(100% - 20px) calc(1px + 50%), calc(100% - 16.1px) calc(1px + 50%)", "background-size": "4px 4px, 4px 4px", "background-repeat": "no-repeat", "text-overflow": "ellipsis", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset", "border-color": "var(--input-color)", "--input-color": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", "--size": "calc(var(--size-field, 0.25rem) * 10)", '[dir="rtl"] &': { "background-position": "calc(0% + 12px) calc(1px + 50%), calc(0% + 16px) calc(1px + 50%)" }, select: { "margin-inline-start": "calc(0.25rem * -4)", "margin-inline-end": "calc(0.25rem * -7)", width: "calc(100% + 2.75rem)", appearance: "none", "padding-inline-start": "calc(0.25rem * 4)", "padding-inline-end": "calc(0.25rem * 7)", height: "calc(100% - 2px)", background: "inherit", "border-radius": "inherit", "border-style": "none", "&:focus, &:focus-within": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, "&:not(:last-child)": { "margin-inline-end": "calc(0.25rem * -5.5)", "background-image": "none" } }, "&:focus, &:focus-within": { "--input-color": "var(--color-base-content)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)", outline: "2px solid var(--input-color)", "outline-offset": "2px", isolation: "isolate", "z-index": 1 }, "&:has(> select[disabled]), &:is(:disabled, [disabled])": { cursor: "not-allowed", "border-color": "var(--color-base-200)", "background-color": "var(--color-base-200)", color: "color-mix(in oklab, var(--color-base-content) 40%, transparent)", "&::placeholder": { color: "color-mix(in oklab, var(--color-base-content) 20%, transparent)" } }, "&:has(> select[disabled]) > select[disabled]": { cursor: "not-allowed" } }, ".select-ghost": { "background-color": "transparent", transition: "background-color 0.2s", "box-shadow": "none", "border-color": "#0000", "&:focus, &:focus-within": { "background-color": "var(--color-base-100)", color: "var(--color-base-content)", "border-color": "#0000", "box-shadow": "none" } }, ".select-neutral": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-neutral)" } }, ".select-primary": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-primary)" } }, ".select-secondary": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-secondary)" } }, ".select-accent": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-accent)" } }, ".select-info": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-info)" } }, ".select-success": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-success)" } }, ".select-warning": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-warning)" } }, ".select-error": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-error)" } }, ".select-xs": { "--size": "calc(var(--size-field, 0.25rem) * 6)", "font-size": "0.6875rem" }, ".select-sm": { "--size": "calc(var(--size-field, 0.25rem) * 8)", "font-size": "0.75rem" }, ".select-md": { "--size": "calc(var(--size-field, 0.25rem) * 10)", "font-size": "0.875rem" }, ".select-lg": { "--size": "calc(var(--size-field, 0.25rem) * 12)", "font-size": "1.125rem" }, ".select-xl": { "--size": "calc(var(--size-field, 0.25rem) * 14)", "font-size": "1.375rem" } };
+
+// packages/daisyui/components/select/index.js
+var select_default = ({ addComponents, prefix = "" }) => {
+ const prefixedselect = addPrefix(object_default20, prefix);
+ addComponents({ ...prefixedselect });
+};
+
+// packages/daisyui/components/progress/object.js
+var object_default21 = { ".progress": { position: "relative", height: "calc(0.25rem * 2)", width: "100%", appearance: "none", overflow: "hidden", "border-radius": "var(--radius-box)", "background-color": "color-mix(in oklab, currentColor 20%, transparent)", color: "var(--color-base-content)", "&:indeterminate": { "background-image": "repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% )", "background-size": "200%", "background-position-x": "15%", animation: "progress 5s ease-in-out infinite", "@supports (-moz-appearance: none)": { "&::-moz-progress-bar": { "background-color": "transparent", "background-image": "repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% )", "background-size": "200%", "background-position-x": "15%", animation: "progress 5s ease-in-out infinite" } } }, "@supports (-moz-appearance: none)": { "&::-moz-progress-bar": { "border-radius": "var(--radius-box)", "background-color": "currentColor" } }, "@supports (-webkit-appearance: none)": { "&::-webkit-progress-bar": { "border-radius": "var(--radius-box)", "background-color": "transparent" }, "&::-webkit-progress-value": { "border-radius": "var(--radius-box)", "background-color": "currentColor" } } }, ".progress-primary": { color: "var(--color-primary)" }, ".progress-secondary": { color: "var(--color-secondary)" }, ".progress-accent": { color: "var(--color-accent)" }, ".progress-neutral": { color: "var(--color-neutral)" }, ".progress-info": { color: "var(--color-info)" }, ".progress-success": { color: "var(--color-success)" }, ".progress-warning": { color: "var(--color-warning)" }, ".progress-error": { color: "var(--color-error)" }, "@keyframes progress": { "50%": { "background-position-x": "-115%" } } };
+
+// packages/daisyui/components/progress/index.js
+var progress_default = ({ addComponents, prefix = "" }) => {
+ const prefixedprogress = addPrefix(object_default21, prefix);
+ addComponents({ ...prefixedprogress });
+};
+
+// packages/daisyui/components/fileinput/object.js
+var object_default22 = { ".file-input": { cursor: ["pointer", "pointer"], border: "var(--border) solid #0000", display: "inline-flex", appearance: "none", "align-items": "center", "background-color": "var(--color-base-100)", "vertical-align": "middle", "webkit-user-select": "none", "user-select": "none", width: "clamp(3rem, 20rem, 100%)", height: "var(--size)", "padding-inline-end": "0.75rem", "font-size": "0.875rem", "line-height": 2, "border-start-start-radius": "var(--join-ss, var(--radius-field))", "border-start-end-radius": "var(--join-se, var(--radius-field))", "border-end-start-radius": "var(--join-es, var(--radius-field))", "border-end-end-radius": "var(--join-ee, var(--radius-field))", "border-color": "var(--input-color)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset", "--size": "calc(var(--size-field, 0.25rem) * 10)", "--input-color": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", "&::file-selector-button": { "margin-inline-end": "calc(0.25rem * 4)", cursor: "pointer", "padding-inline": "calc(0.25rem * 4)", "webkit-user-select": "none", "user-select": "none", height: "calc(100% + var(--border) * 2)", "margin-block": "calc(var(--border) * -1)", "margin-inline-start": "calc(var(--border) * -1)", "font-size": "0.875rem", color: "var(--btn-fg)", "border-width": "var(--border)", "border-style": "solid", "border-color": "var(--btn-border)", "border-start-start-radius": "calc(var(--join-ss, var(--radius-field) - var(--border)))", "border-end-start-radius": "calc(var(--join-es, var(--radius-field) - var(--border)))", "font-weight": 600, "background-color": "var(--btn-bg)", "background-size": "calc(var(--noise) * 100%)", "background-image": "var(--btn-noise)", "text-shadow": "0 0.5px oklch(1 0 0 / calc(var(--depth) * 0.15))", "box-shadow": "0 0.5px 0 0.5px color-mix( in oklab, color-mix(in oklab, white 30%, var(--btn-bg)) calc(var(--depth) * 20%), #0000 ) inset, var(--btn-shadow)", "--size": "calc(var(--size-field, 0.25rem) * 10)", "--btn-bg": "var(--btn-color, var(--color-base-200))", "--btn-fg": "var(--color-base-content)", "--btn-border": "color-mix(in oklab, var(--btn-bg), #000 5%)", "--btn-shadow": `0 3px 2px -2px color-mix(in oklab, var(--btn-bg) 30%, #0000),
+ 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) 30%, #0000)`, "--btn-noise": "var(--fx-noise)" }, "&:focus": { "--input-color": "var(--color-base-content)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) 10%, #0000)", outline: "2px solid var(--input-color)", "outline-offset": "2px", isolation: "isolate" }, "&:has(> input[disabled]), &:is(:disabled, [disabled])": { cursor: "not-allowed", "border-color": "var(--color-base-200)", "background-color": "var(--color-base-200)", "&::placeholder": { color: "color-mix(in oklab, var(--color-base-content) 20%, transparent)" }, "box-shadow": "none", color: "color-mix(in oklch, var(--color-base-content) 20%, #0000)", "&::file-selector-button": { cursor: "not-allowed", "border-color": "var(--color-base-200)", "background-color": "var(--color-base-200)", "--btn-border": "#0000", "--btn-noise": "none", "--btn-fg": "color-mix(in oklch, var(--color-base-content) 20%, #0000)" } } }, ".file-input-ghost": { "background-color": "transparent", transition: "background-color 0.2s", "box-shadow": "none", "border-color": "#0000", "&::file-selector-button": { "margin-inline-start": "calc(0.25rem * 0)", "margin-inline-end": "calc(0.25rem * 4)", height: "100%", cursor: "pointer", "padding-inline": "calc(0.25rem * 4)", "webkit-user-select": "none", "user-select": "none", "margin-block": "0", "border-start-end-radius": "calc(var(--join-ss, var(--radius-field) - var(--border)))", "border-end-end-radius": "calc(var(--join-es, var(--radius-field) - var(--border)))" }, "&:focus, &:focus-within": { "background-color": "var(--color-base-100)", color: "var(--color-base-content)", "border-color": "#0000", "box-shadow": "none" } }, ".file-input-neutral": { "--btn-color": "var(--color-neutral)", "&::file-selector-button": { color: "var(--color-neutral-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-neutral)" } }, ".file-input-primary": { "--btn-color": "var(--color-primary)", "&::file-selector-button": { color: "var(--color-primary-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-primary)" } }, ".file-input-secondary": { "--btn-color": "var(--color-secondary)", "&::file-selector-button": { color: "var(--color-secondary-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-secondary)" } }, ".file-input-accent": { "--btn-color": "var(--color-accent)", "&::file-selector-button": { color: "var(--color-accent-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-accent)" } }, ".file-input-info": { "--btn-color": "var(--color-info)", "&::file-selector-button": { color: "var(--color-info-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-info)" } }, ".file-input-success": { "--btn-color": "var(--color-success)", "&::file-selector-button": { color: "var(--color-success-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-success)" } }, ".file-input-warning": { "--btn-color": "var(--color-warning)", "&::file-selector-button": { color: "var(--color-warning-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-warning)" } }, ".file-input-error": { "--btn-color": "var(--color-error)", "&::file-selector-button": { color: "var(--color-error-content)" }, "&, &:focus, &:focus-within": { "--input-color": "var(--color-error)" } }, ".file-input-xs": { "--size": "calc(var(--size-field, 0.25rem) * 6)", "font-size": "0.6875rem", "line-height": "1rem", "&::file-selector-button": { "font-size": "0.6875rem" } }, ".file-input-sm": { "--size": "calc(var(--size-field, 0.25rem) * 8)", "font-size": "0.75rem", "line-height": "1.5rem", "&::file-selector-button": { "font-size": "0.75rem" } }, ".file-input-md": { "--size": "calc(var(--size-field, 0.25rem) * 10)", "font-size": "0.875rem", "line-height": 2, "&::file-selector-button": { "font-size": "0.875rem" } }, ".file-input-lg": { "--size": "calc(var(--size-field, 0.25rem) * 12)", "font-size": "1.125rem", "line-height": "2.5rem", "&::file-selector-button": { "font-size": "1.125rem" } }, ".file-input-xl": { "padding-inline-end": "calc(0.25rem * 6)", "--size": "calc(var(--size-field, 0.25rem) * 14)", "font-size": "1.125rem", "line-height": "3rem", "&::file-selector-button": { "font-size": "1.375rem" } } };
+
+// packages/daisyui/components/fileinput/index.js
+var fileinput_default = ({ addComponents, prefix = "" }) => {
+ const prefixedfileinput = addPrefix(object_default22, prefix);
+ addComponents({ ...prefixedfileinput });
+};
+
+// packages/daisyui/components/modal/object.js
+var object_default23 = { ".modal": { "pointer-events": "none", visibility: "hidden", position: "fixed", inset: "calc(0.25rem * 0)", margin: "calc(0.25rem * 0)", display: "grid", height: "100%", "max-height": "none", width: "100%", "max-width": "none", "align-items": "center", "justify-items": "center", "background-color": "transparent", padding: "calc(0.25rem * 0)", color: "inherit", "overflow-x": "hidden", transition: "translate 0.3s ease-out, visibility 0.3s allow-discrete, background-color 0.3s ease-out, opacity 0.1s ease-out", "overflow-y": "hidden", "overscroll-behavior": "contain", "z-index": 999, "&::backdrop": { display: "none" }, "&.modal-open, &[open], &:target": { "pointer-events": "auto", visibility: "visible", opacity: "100%", "background-color": "oklch(0% 0 0/ 0.4)", ".modal-box": { translate: "0 0", scale: "1", opacity: 1 } }, "@starting-style": { "&.modal-open, &[open], &:target": { visibility: "hidden", opacity: "0%" } } }, ".modal-action": { "margin-top": "calc(0.25rem * 6)", display: "flex", "justify-content": "flex-end", gap: "calc(0.25rem * 2)" }, ".modal-toggle": { position: "fixed", height: "calc(0.25rem * 0)", width: "calc(0.25rem * 0)", appearance: "none", opacity: "0%", "&:checked + .modal": { "pointer-events": "auto", visibility: "visible", opacity: "100%", "background-color": "oklch(0% 0 0/ 0.4)", ".modal-box": { translate: "0 0", scale: "1", opacity: 1 } }, "@starting-style": { "&:checked + .modal": { visibility: "hidden", opacity: "0%" } } }, ".modal-backdrop": { "grid-column-start": "1", "grid-row-start": "1", display: "grid", "align-self": "stretch", "justify-self": "stretch", color: "transparent", "z-index": -1, button: { cursor: "pointer" } }, ".modal-box": { "grid-column-start": "1", "grid-row-start": "1", "max-height": "100vh", width: "calc(11/12 * 100%)", "max-width": "32rem", "background-color": "var(--color-base-100)", padding: "calc(0.25rem * 6)", transition: "translate 0.3s ease-out, scale 0.3s ease-out, opacity 0.2s ease-out 0.05s, box-shadow 0.3s ease-out", "border-top-left-radius": "var(--modal-tl, var(--radius-box))", "border-top-right-radius": "var(--modal-tr, var(--radius-box))", "border-bottom-left-radius": "var(--modal-bl, var(--radius-box))", "border-bottom-right-radius": "var(--modal-br, var(--radius-box))", scale: "95%", opacity: 0, "box-shadow": "oklch(0% 0 0/ 0.25) 0px 25px 50px -12px", "overflow-y": "auto", "overscroll-behavior": "contain" }, ".modal-top": { "place-items": "start", ":where(.modal-box)": { height: "auto", width: "100%", "max-width": "none", "max-height": "calc(100vh - 5em)", translate: "0 -100%", scale: "1", "--modal-tl": "0", "--modal-tr": "0", "--modal-bl": "var(--radius-box)", "--modal-br": "var(--radius-box)" } }, ".modal-middle": { "place-items": "center", ":where(.modal-box)": { height: "auto", width: "calc(11/12 * 100%)", "max-width": "32rem", "max-height": "calc(100vh - 5em)", translate: "0 2%", scale: "98%", "--modal-tl": "var(--radius-box)", "--modal-tr": "var(--radius-box)", "--modal-bl": "var(--radius-box)", "--modal-br": "var(--radius-box)" } }, ".modal-bottom": { "place-items": "end", ":where(.modal-box)": { height: "auto", width: "100%", "max-width": "none", "max-height": "calc(100vh - 5em)", translate: "0 100%", scale: "1", "--modal-tl": "var(--radius-box)", "--modal-tr": "var(--radius-box)", "--modal-bl": "0", "--modal-br": "0" } }, ".modal-start": { "place-items": "start", ":where(.modal-box)": { height: "100vh", "max-height": "none", width: "auto", "max-width": "none", translate: "-100% 0", scale: "1", "--modal-tl": "0", "--modal-tr": "var(--radius-box)", "--modal-bl": "0", "--modal-br": "var(--radius-box)" } }, ".modal-end": { "place-items": "end", ":where(.modal-box)": { height: "100vh", "max-height": "none", width: "auto", "max-width": "none", translate: "100% 0", scale: "1", "--modal-tl": "var(--radius-box)", "--modal-tr": "0", "--modal-bl": "var(--radius-box)", "--modal-br": "0" } } };
+
+// packages/daisyui/components/modal/index.js
+var modal_default = ({ addComponents, prefix = "" }) => {
+ const prefixedmodal = addPrefix(object_default23, prefix);
+ addComponents({ ...prefixedmodal });
+};
+
+// packages/daisyui/components/footer/object.js
+var object_default24 = { ".footer": { display: "grid", width: "100%", "grid-auto-flow": "row", "place-items": "start", "column-gap": "calc(0.25rem * 4)", "row-gap": "calc(0.25rem * 10)", "font-size": "0.875rem", "line-height": "1.25rem", "& > *": { display: "grid", "place-items": "start", gap: "calc(0.25rem * 2)" }, "&.footer-center": { "grid-auto-flow": "column dense", "place-items": "center", "text-align": "center", "& > *": { "place-items": "center" } } }, ".footer-title": { "margin-bottom": "calc(0.25rem * 2)", "text-transform": "uppercase", opacity: "60%", "font-weight": 600 }, ".footer-horizontal": { "grid-auto-flow": "column", "&.footer-center": { "grid-auto-flow": "row dense" } }, ".footer-vertical": { "grid-auto-flow": "row", "&.footer-center": { "grid-auto-flow": "column dense" } } };
+
+// packages/daisyui/components/footer/index.js
+var footer_default = ({ addComponents, prefix = "" }) => {
+ const prefixedfooter = addPrefix(object_default24, prefix);
+ addComponents({ ...prefixedfooter });
+};
+
+// packages/daisyui/components/table/object.js
+var object_default25 = { ".table": { "font-size": "0.875rem", position: "relative", width: "100%", "border-radius": "var(--radius-box)", "text-align": "left", '&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)': { "text-align": "right" }, "tr.row-hover": { "&, &:nth-child(even)": { "&:hover": { "@media (hover: hover)": { "background-color": "var(--color-base-200)" } } } }, ":where(th, td)": { "padding-inline": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 3)", "vertical-align": "middle" }, ":where(thead, tfoot)": { "white-space": "nowrap", color: "color-mix(in oklab, var(--color-base-content) 60%, transparent)", "font-size": "0.875rem", "font-weight": 600 }, ":where(tfoot)": { "border-top": "var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)" }, ":where(.table-pin-rows thead tr)": { position: "sticky", top: "calc(0.25rem * 0)", "z-index": 1, "background-color": "var(--color-base-100)" }, ":where(.table-pin-rows tfoot tr)": { position: "sticky", bottom: "calc(0.25rem * 0)", "z-index": 1, "background-color": "var(--color-base-100)" }, ":where(.table-pin-cols tr th)": { position: "sticky", right: "calc(0.25rem * 0)", left: "calc(0.25rem * 0)", "background-color": "var(--color-base-100)" }, ":where(thead tr, tbody tr:not(:last-child))": { "border-bottom": "var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)" } }, ".table-zebra": { tbody: { tr: { "&:where(:nth-child(even))": { "background-color": "var(--color-base-200)", ":where(.table-pin-cols tr th)": { "background-color": "var(--color-base-200)" } }, "&.row-hover": { "&, &:where(:nth-child(even))": { "&:hover": { "@media (hover: hover)": { "background-color": "var(--color-base-300)" } } } } } } }, ".table-xs": { ":not(thead, tfoot) tr": { "font-size": "0.6875rem" }, ":where(th, td)": { "padding-inline": "calc(0.25rem * 2)", "padding-block": "calc(0.25rem * 1)" } }, ".table-sm": { ":not(thead, tfoot) tr": { "font-size": "0.75rem" }, ":where(th, td)": { "padding-inline": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 2)" } }, ".table-md": { ":not(thead, tfoot) tr": { "font-size": "0.875rem" }, ":where(th, td)": { "padding-inline": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 3)" } }, ".table-lg": { ":not(thead, tfoot) tr": { "font-size": "1.125rem" }, ":where(th, td)": { "padding-inline": "calc(0.25rem * 5)", "padding-block": "calc(0.25rem * 4)" } }, ".table-xl": { ":not(thead, tfoot) tr": { "font-size": "1.375rem" }, ":where(th, td)": { "padding-inline": "calc(0.25rem * 6)", "padding-block": "calc(0.25rem * 5)" } } };
+
+// packages/daisyui/components/table/index.js
+var table_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtable = addPrefix(object_default25, prefix);
+ addComponents({ ...prefixedtable });
+};
+
+// packages/daisyui/components/avatar/object.js
+var object_default26 = { ".avatar-group": { display: "flex", overflow: "hidden", ":where(.avatar)": { overflow: "hidden", "border-radius": "calc(infinity * 1px)", border: "4px solid var(--color-base-100)" } }, ".avatar": { position: "relative", display: "inline-flex", "vertical-align": "middle", "& > div": { display: "block", "aspect-ratio": "1 / 1", overflow: "hidden" }, img: { height: "100%", width: "100%", "object-fit": "cover" } }, ".avatar-placeholder": { "& > div": { display: "flex", "align-items": "center", "justify-content": "center" } }, ".avatar-online": { "&:before": { content: '""', position: "absolute", "z-index": 1, display: "block", "border-radius": "calc(infinity * 1px)", "background-color": "var(--color-success)", outline: "2px solid var(--color-base-100)", width: "15%", height: "15%", top: "7%", right: "7%" } }, ".avatar-offline": { "&:before": { content: '""', position: "absolute", "z-index": 1, display: "block", "border-radius": "calc(infinity * 1px)", "background-color": "var(--color-base-300)", outline: "2px solid var(--color-base-100)", width: "15%", height: "15%", top: "7%", right: "7%" } } };
+
+// packages/daisyui/components/avatar/index.js
+var avatar_default = ({ addComponents, prefix = "" }) => {
+ const prefixedavatar = addPrefix(object_default26, prefix);
+ addComponents({ ...prefixedavatar });
+};
+
+// packages/daisyui/components/input/object.js
+var object_default27 = { ".input": { cursor: "text", border: "var(--border) solid #0000", position: "relative", display: "inline-flex", "flex-shrink": 1, appearance: "none", "align-items": "center", gap: "calc(0.25rem * 2)", "background-color": "var(--color-base-100)", "padding-inline": "calc(0.25rem * 3)", "vertical-align": "middle", "white-space": "nowrap", width: "clamp(3rem, 20rem, 100%)", height: "var(--size)", "font-size": "0.875rem", "border-start-start-radius": "var(--join-ss, var(--radius-field))", "border-start-end-radius": "var(--join-se, var(--radius-field))", "border-end-start-radius": "var(--join-es, var(--radius-field))", "border-end-end-radius": "var(--join-ee, var(--radius-field))", "border-color": "var(--input-color)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset", "--size": "calc(var(--size-field, 0.25rem) * 10)", "--input-color": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", "&:where(input)": { display: "inline-flex" }, ":where(input)": { display: "inline-flex", height: "100%", width: "100%", appearance: "none", "background-color": "transparent", border: "none", "&:focus, &:focus-within": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } } }, ':where(input[type="date"])': { display: "inline-block" }, "&:focus, &:focus-within": { "--input-color": "var(--color-base-content)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)", outline: "2px solid var(--input-color)", "outline-offset": "2px", isolation: "isolate", "z-index": 1 }, "&:has(> input[disabled]), &:is(:disabled, [disabled])": { cursor: "not-allowed", "border-color": "var(--color-base-200)", "background-color": "var(--color-base-200)", color: "color-mix(in oklab, var(--color-base-content) 40%, transparent)", "&::placeholder": { color: "color-mix(in oklab, var(--color-base-content) 20%, transparent)" }, "box-shadow": "none" }, "&:has(> input[disabled]) > input[disabled]": { cursor: "not-allowed" }, "&::-webkit-date-and-time-value": { "text-align": "inherit" }, '&[type="number"]': { "&::-webkit-inner-spin-button": { "margin-block": "calc(0.25rem * -3)", "margin-inline-end": "calc(0.25rem * -3)" } }, "&::-webkit-calendar-picker-indicator": { position: "absolute", "inset-inline-end": "0.75em" } }, ".input-ghost": { "background-color": "transparent", "box-shadow": "none", "border-color": "#0000", "&:focus, &:focus-within": { "background-color": "var(--color-base-100)", color: "var(--color-base-content)", "border-color": "#0000", "box-shadow": "none" } }, ".input-neutral": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-neutral)" } }, ".input-primary": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-primary)" } }, ".input-secondary": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-secondary)" } }, ".input-accent": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-accent)" } }, ".input-info": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-info)" } }, ".input-success": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-success)" } }, ".input-warning": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-warning)" } }, ".input-error": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-error)" } }, ".input-xs": { "--size": "calc(var(--size-field, 0.25rem) * 6)", "font-size": "0.6875rem", '&[type="number"]': { "&::-webkit-inner-spin-button": { "margin-block": "calc(0.25rem * -1)", "margin-inline-end": "calc(0.25rem * -3)" } } }, ".input-sm": { "--size": "calc(var(--size-field, 0.25rem) * 8)", "font-size": "0.75rem", '&[type="number"]': { "&::-webkit-inner-spin-button": { "margin-block": "calc(0.25rem * -2)", "margin-inline-end": "calc(0.25rem * -3)" } } }, ".input-md": { "--size": "calc(var(--size-field, 0.25rem) * 10)", "font-size": "0.875rem", '&[type="number"]': { "&::-webkit-inner-spin-button": { "margin-block": "calc(0.25rem * -3)", "margin-inline-end": "calc(0.25rem * -3)" } } }, ".input-lg": { "--size": "calc(var(--size-field, 0.25rem) * 12)", "font-size": "1.125rem", '&[type="number"]': { "&::-webkit-inner-spin-button": { "margin-block": "calc(0.25rem * -3)", "margin-inline-end": "calc(0.25rem * -3)" } } }, ".input-xl": { "--size": "calc(var(--size-field, 0.25rem) * 14)", "font-size": "1.375rem", '&[type="number"]': { "&::-webkit-inner-spin-button": { "margin-block": "calc(0.25rem * -4)", "margin-inline-end": "calc(0.25rem * -3)" } } } };
+
+// packages/daisyui/components/input/index.js
+var input_default = ({ addComponents, prefix = "" }) => {
+ const prefixedinput = addPrefix(object_default27, prefix);
+ addComponents({ ...prefixedinput });
+};
+
+// packages/daisyui/components/checkbox/object.js
+var object_default28 = { ".checkbox": { border: "var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000))", position: "relative", "flex-shrink": 0, cursor: "pointer", appearance: "none", "border-radius": "var(--radius-selector)", padding: "calc(0.25rem * 1)", "vertical-align": "middle", color: "var(--color-base-content)", "box-shadow": "0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0 #0000 inset, 0 0 #0000", transition: "background-color 0.2s, box-shadow 0.2s", "--size": "calc(var(--size-selector, 0.25rem) * 6)", width: "var(--size)", height: "var(--size)", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--fx-noise)", "&:before": { "--tw-content": '""', content: "var(--tw-content)", display: "block", width: "100%", height: "100%", rotate: "45deg", "background-color": "currentColor", opacity: "0%", transition: "clip-path 0.3s, opacity 0.1s, rotate 0.3s, translate 0.3s", "transition-delay": "0.1s", "clip-path": "polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 70% 80%, 70% 100%)", "box-shadow": "0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset", "font-size": "1rem", "line-height": 0.75 }, "&:focus-visible": { outline: "2px solid var(--input-color, currentColor)", "outline-offset": "2px" }, '&:checked, &[aria-checked="true"]': { "background-color": "var(--input-color, #0000)", "box-shadow": "0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1))", "&:before": { "clip-path": "polygon(20% 100%, 20% 80%, 50% 80%, 50% 0%, 70% 0%, 70% 100%)", opacity: "100%" }, "@media (forced-colors: active)": { "&:before": { rotate: "0deg", "background-color": "transparent", "--tw-content": '"✔︎"', "clip-path": "none" } }, "@media print": { "&:before": { rotate: "0deg", "background-color": "transparent", "--tw-content": '"✔︎"', "clip-path": "none" } } }, "&:indeterminate": { "&:before": { rotate: "0deg", opacity: "100%", translate: "0 -35%", "clip-path": "polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 80% 80%, 80% 100%)" } } }, ".checkbox-primary": { color: "var(--color-primary-content)", "--input-color": "var(--color-primary)" }, ".checkbox-secondary": { color: "var(--color-secondary-content)", "--input-color": "var(--color-secondary)" }, ".checkbox-accent": { color: "var(--color-accent-content)", "--input-color": "var(--color-accent)" }, ".checkbox-neutral": { color: "var(--color-neutral-content)", "--input-color": "var(--color-neutral)" }, ".checkbox-info": { color: "var(--color-info-content)", "--input-color": "var(--color-info)" }, ".checkbox-success": { color: "var(--color-success-content)", "--input-color": "var(--color-success)" }, ".checkbox-warning": { color: "var(--color-warning-content)", "--input-color": "var(--color-warning)" }, ".checkbox-error": { color: "var(--color-error-content)", "--input-color": "var(--color-error)" }, ".checkbox:disabled": { cursor: "not-allowed", opacity: "20%" }, ".checkbox-xs": { padding: "0.125rem", "--size": "calc(var(--size-selector, 0.25rem) * 4)" }, ".checkbox-sm": { padding: "0.1875rem", "--size": "calc(var(--size-selector, 0.25rem) * 5)" }, ".checkbox-md": { padding: "0.25rem", "--size": "calc(var(--size-selector, 0.25rem) * 6)" }, ".checkbox-lg": { padding: "0.3125rem", "--size": "calc(var(--size-selector, 0.25rem) * 7)" }, ".checkbox-xl": { padding: "0.375rem", "--size": "calc(var(--size-selector, 0.25rem) * 8)" } };
+
+// packages/daisyui/components/checkbox/index.js
+var checkbox_default = ({ addComponents, prefix = "" }) => {
+ const prefixedcheckbox = addPrefix(object_default28, prefix);
+ addComponents({ ...prefixedcheckbox });
+};
+
+// packages/daisyui/components/badge/object.js
+var object_default29 = { ".badge": { display: "inline-flex", "align-items": "center", "justify-content": "center", gap: "calc(0.25rem * 2)", "border-radius": "var(--radius-selector)", "vertical-align": "middle", color: "var(--badge-fg)", border: "var(--border) solid var(--badge-color, var(--color-base-200))", "font-size": "0.875rem", width: "fit-content", "padding-inline": "calc(0.25rem * 3 - var(--border))", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--fx-noise)", "background-color": "var(--badge-bg)", "--badge-bg": "var(--badge-color, var(--color-base-100))", "--badge-fg": "var(--color-base-content)", "--size": "calc(var(--size-selector, 0.25rem) * 6)", height: "var(--size)", "&.badge-outline": { "--badge-fg": "var(--badge-color)", "--badge-bg": "#0000", "background-image": "none", "border-color": "currentColor" }, "&.badge-dash": { "--badge-fg": "var(--badge-color)", "--badge-bg": "#0000", "background-image": "none", "border-color": "currentColor", "border-style": "dashed" }, "&.badge-soft": { color: "var(--badge-color, var(--color-base-content))", "background-color": "color-mix( in oklab, var(--badge-color, var(--color-base-content)) 8%, var(--color-base-100) )", "border-color": "color-mix( in oklab, var(--badge-color, var(--color-base-content)) 10%, var(--color-base-100) )", "background-image": "none" } }, ".badge-primary": { "--badge-color": "var(--color-primary)", "--badge-fg": "var(--color-primary-content)" }, ".badge-secondary": { "--badge-color": "var(--color-secondary)", "--badge-fg": "var(--color-secondary-content)" }, ".badge-accent": { "--badge-color": "var(--color-accent)", "--badge-fg": "var(--color-accent-content)" }, ".badge-neutral": { "--badge-color": "var(--color-neutral)", "--badge-fg": "var(--color-neutral-content)" }, ".badge-info": { "--badge-color": "var(--color-info)", "--badge-fg": "var(--color-info-content)" }, ".badge-success": { "--badge-color": "var(--color-success)", "--badge-fg": "var(--color-success-content)" }, ".badge-warning": { "--badge-color": "var(--color-warning)", "--badge-fg": "var(--color-warning-content)" }, ".badge-error": { "--badge-color": "var(--color-error)", "--badge-fg": "var(--color-error-content)" }, ".badge-ghost": { "border-color": "var(--color-base-200)", "background-color": "var(--color-base-200)", color: "var(--color-base-content)", "background-image": "none" }, ".badge-xs": { "--size": "calc(var(--size-selector, 0.25rem) * 4)", "font-size": "0.625rem", "padding-inline": "calc(0.25rem * 2 - var(--border))" }, ".badge-sm": { "--size": "calc(var(--size-selector, 0.25rem) * 5)", "font-size": "0.75rem", "padding-inline": "calc(0.25rem * 2.5 - var(--border))" }, ".badge-md": { "--size": "calc(var(--size-selector, 0.25rem) * 6)", "font-size": "0.875rem", "padding-inline": "calc(0.25rem * 3 - var(--border))" }, ".badge-lg": { "--size": "calc(var(--size-selector, 0.25rem) * 7)", "font-size": "1rem", "padding-inline": "calc(0.25rem * 3.5 - var(--border))" }, ".badge-xl": { "--size": "calc(var(--size-selector, 0.25rem) * 8)", "font-size": "1.125rem", "padding-inline": "calc(0.25rem * 4 - var(--border))" } };
+
+// packages/daisyui/components/badge/index.js
+var badge_default = ({ addComponents, prefix = "" }) => {
+ const prefixedbadge = addPrefix(object_default29, prefix);
+ addComponents({ ...prefixedbadge });
+};
+
+// packages/daisyui/components/status/object.js
+var object_default30 = { ".status": { display: "inline-block", "aspect-ratio": "1 / 1", width: "calc(0.25rem * 2)", height: "calc(0.25rem * 2)", "border-radius": "var(--radius-selector)", "background-color": "color-mix(in oklab, var(--color-base-content) 20%, transparent)", "background-position": "center", "background-repeat": "no-repeat", "vertical-align": "middle", color: "color-mix(in srgb, #000 30%, transparent)", "@supports (color: color-mix(in lab, red, red))": { color: "color-mix(in oklab, var(--color-black) 30%, transparent)" }, "background-image": "radial-gradient( circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), #0000 )", "box-shadow": "0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000)" }, ".status-primary": { "background-color": "var(--color-primary)", color: "var(--color-primary)" }, ".status-secondary": { "background-color": "var(--color-secondary)", color: "var(--color-secondary)" }, ".status-accent": { "background-color": "var(--color-accent)", color: "var(--color-accent)" }, ".status-neutral": { "background-color": "var(--color-neutral)", color: "var(--color-neutral)" }, ".status-info": { "background-color": "var(--color-info)", color: "var(--color-info)" }, ".status-success": { "background-color": "var(--color-success)", color: "var(--color-success)" }, ".status-warning": { "background-color": "var(--color-warning)", color: "var(--color-warning)" }, ".status-error": { "background-color": "var(--color-error)", color: "var(--color-error)" }, ".status-xs": { width: "calc(0.25rem * 0.5)", height: "calc(0.25rem * 0.5)" }, ".status-sm": { width: "calc(0.25rem * 1)", height: "calc(0.25rem * 1)" }, ".status-md": { width: "calc(0.25rem * 2)", height: "calc(0.25rem * 2)" }, ".status-lg": { width: "calc(0.25rem * 3)", height: "calc(0.25rem * 3)" }, ".status-xl": { width: "calc(0.25rem * 4)", height: "calc(0.25rem * 4)" } };
+
+// packages/daisyui/components/status/index.js
+var status_default = ({ addComponents, prefix = "" }) => {
+ const prefixedstatus = addPrefix(object_default30, prefix);
+ addComponents({ ...prefixedstatus });
+};
+
+// packages/daisyui/components/diff/object.js
+var object_default31 = { ".diff": { position: "relative", display: "grid", width: "100%", overflow: "hidden", "webkit-user-select": "none", "user-select": "none", direction: "ltr", "container-type": "inline-size", "grid-template-columns": "auto 1fr", "&:focus-visible, &:has(.diff-item-1:focus-visible)": { "outline-style": "var(--tw-outline-style)", "outline-width": "2px", "outline-offset": "1px", "outline-color": "var(--color-base-content)" }, "&:focus-visible": { "outline-style": "var(--tw-outline-style)", "outline-width": "2px", "outline-offset": "1px", "outline-color": "var(--color-base-content)", ".diff-resizer": { "min-width": "90cqi", "max-width": "90cqi" } }, "&:has(.diff-item-2:focus-visible)": { "outline-style": "var(--tw-outline-style)", "outline-width": "2px", "outline-offset": "1px", ".diff-resizer": { "min-width": "10cqi", "max-width": "10cqi" } }, "@supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x)": { "&:focus": { ".diff-resizer": { "min-width": "10cqi", "max-width": "10cqi" } }, "&:has(.diff-item-1:focus)": { ".diff-resizer": { "min-width": "90cqi", "max-width": "90cqi" } } } }, ".diff-resizer": { position: "relative", top: "calc(1/2 * 100%)", "z-index": 1, "grid-column-start": "1", "grid-row-start": "1", height: "calc(0.25rem * 2)", width: "50cqi", "max-width": "calc(100cqi - 1rem)", "min-width": "1rem", resize: "horizontal", overflow: "hidden", opacity: "0%", transform: "scaleY(3) translate(0.35rem, 0.08rem)", cursor: "ew-resize", "transform-origin": "100% 100%", "clip-path": "inset(calc(100% - 0.75rem) 0 0 calc(100% - 0.75rem))", transition: "min-width 0.3s ease-out, max-width 0.3s ease-out" }, ".diff-item-2": { position: "relative", "grid-column-start": "1", "grid-row-start": "1", "&:after": { "pointer-events": "none", position: "absolute", top: "calc(1/2 * 100%)", right: "1px", bottom: "calc(0.25rem * 0)", "z-index": 2, "border-radius": "calc(infinity * 1px)", "background-color": "color-mix(in oklab, var(--color-base-100) 50%, transparent)", width: "1.2rem", height: "1.8rem", border: "2px solid var(--color-base-100)", content: '""', outline: "1px solid color-mix(in oklab, var(--color-base-content) 5%, #0000)", "outline-offset": "-3px", "backdrop-filter": "blur(8px)", "box-shadow": "0 1px 2px 0 oklch(0% 0 0 / 0.1)", translate: "50% -50%" }, "> *": { "pointer-events": "none", position: "absolute", top: "calc(0.25rem * 0)", bottom: "calc(0.25rem * 0)", left: "calc(0.25rem * 0)", height: "100%", width: "100cqi", "max-width": "none", "object-fit": "cover", "object-position": "center" }, "@supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x)": { "&:after": { content: "none" } } }, ".diff-item-1": { position: "relative", "z-index": 1, "grid-column-start": "1", "grid-row-start": "1", overflow: "hidden", "border-right": "2px solid var(--color-base-100)", "> *": { "pointer-events": "none", position: "absolute", top: "calc(0.25rem * 0)", bottom: "calc(0.25rem * 0)", left: "calc(0.25rem * 0)", height: "100%", width: "100cqi", "max-width": "none", "object-fit": "cover", "object-position": "center" } } };
+
+// packages/daisyui/components/diff/index.js
+var diff_default = ({ addComponents, prefix = "" }) => {
+ const prefixeddiff = addPrefix(object_default31, prefix);
+ addComponents({ ...prefixeddiff });
+};
+
+// packages/daisyui/components/hero/object.js
+var object_default32 = { ".hero": { display: "grid", width: "100%", "place-items": "center", "background-size": "cover", "background-position": "center", "& > *": { "grid-column-start": "1", "grid-row-start": "1" } }, ".hero-overlay": { "grid-column-start": "1", "grid-row-start": "1", height: "100%", width: "100%", "background-color": "color-mix(in oklab, var(--color-neutral) 50%, transparent)" }, ".hero-content": { isolation: "isolate", display: "flex", "max-width": "80rem", "align-items": "center", "justify-content": "center", gap: "calc(0.25rem * 4)", padding: "calc(0.25rem * 4)" } };
+
+// packages/daisyui/components/hero/index.js
+var hero_default = ({ addComponents, prefix = "" }) => {
+ const prefixedhero = addPrefix(object_default32, prefix);
+ addComponents({ ...prefixedhero });
+};
+
+// packages/daisyui/components/toggle/object.js
+var object_default33 = { ".toggle": { border: "var(--border) solid currentColor", color: "var(--input-color)", position: "relative", display: "inline-grid", "flex-shrink": 0, cursor: "pointer", appearance: "none", "place-content": "center", "vertical-align": "middle", "webkit-user-select": "none", "user-select": "none", "grid-template-columns": "0fr 1fr 1fr", "--radius-selector-max": `calc(
+ var(--radius-selector) + var(--radius-selector) + var(--radius-selector)
+ )`, "border-radius": "calc( var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)) )", padding: "var(--toggle-p)", "box-shadow": "0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset", transition: "color 0.3s, grid-template-columns 0.2s", "--input-color": "color-mix(in oklab, var(--color-base-content) 50%, #0000)", "--toggle-p": "calc(var(--size) * 0.125)", "--size": "calc(var(--size-selector, 0.25rem) * 6)", width: "calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2)", height: "var(--size)", "> *": { "z-index": 1, "grid-column": "span 1 / span 1", "grid-column-start": "2", "grid-row-start": "1", height: "100%", cursor: "pointer", appearance: "none", "background-color": "transparent", padding: "calc(0.25rem * 0.5)", transition: "opacity 0.2s, rotate 0.4s", border: "none", "&:focus": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, "&:nth-child(2)": { color: "var(--color-base-100)", rotate: "0deg" }, "&:nth-child(3)": { color: "var(--color-base-100)", opacity: "0%", rotate: "-15deg" } }, "&:has(:checked)": { "> :nth-child(2)": { opacity: "0%", rotate: "15deg" }, "> :nth-child(3)": { opacity: "100%", rotate: "0deg" } }, "&:before": { position: "relative", "inset-inline-start": "calc(0.25rem * 0)", "grid-column-start": "2", "grid-row-start": "1", "aspect-ratio": "1 / 1", height: "100%", "border-radius": "var(--radius-selector)", "background-color": "currentColor", translate: "0", "--tw-content": '""', content: "var(--tw-content)", transition: "background-color 0.1s, translate 0.2s, inset-inline-start 0.2s", "box-shadow": "0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--fx-noise)" }, "@media (forced-colors: active)": { "&:before": { "outline-style": "var(--tw-outline-style)", "outline-width": "1px", "outline-offset": "calc(1px * -1)" } }, "@media print": { "&:before": { outline: "0.25rem solid", "outline-offset": "-1rem" } }, "&:focus-visible, &:has(:focus-visible)": { outline: "2px solid currentColor", "outline-offset": "2px" }, '&:checked, &[aria-checked="true"], &:has(> input:checked)': { "grid-template-columns": "1fr 1fr 0fr", "background-color": "var(--color-base-100)", "--input-color": "var(--color-base-content)", "&:before": { "background-color": "currentColor" }, "@starting-style": { "&:before": { opacity: 0 } } }, "&:indeterminate": { "grid-template-columns": "0.5fr 1fr 0.5fr" }, "&:disabled": { cursor: "not-allowed", opacity: "30%", "&:before": { "background-color": "transparent", border: "var(--border) solid currentColor" } } }, ".toggle-primary": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-primary)" } }, ".toggle-secondary": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-secondary)" } }, ".toggle-accent": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-accent)" } }, ".toggle-neutral": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-neutral)" } }, ".toggle-success": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-success)" } }, ".toggle-warning": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-warning)" } }, ".toggle-info": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-info)" } }, ".toggle-error": { '&:checked, &[aria-checked="true"]': { "--input-color": "var(--color-error)" } }, ".toggle-xs": { '&:is([type="checkbox"]), &:has([type="checkbox"])': { "--size": "calc(var(--size-selector, 0.25rem) * 4)" } }, ".toggle-sm": { '&:is([type="checkbox"]), &:has([type="checkbox"])': { "--size": "calc(var(--size-selector, 0.25rem) * 5)" } }, ".toggle-md": { '&:is([type="checkbox"]), &:has([type="checkbox"])': { "--size": "calc(var(--size-selector, 0.25rem) * 6)" } }, ".toggle-lg": { '&:is([type="checkbox"]), &:has([type="checkbox"])': { "--size": "calc(var(--size-selector, 0.25rem) * 7)" } }, ".toggle-xl": { '&:is([type="checkbox"]), &:has([type="checkbox"])': { "--size": "calc(var(--size-selector, 0.25rem) * 8)" } } };
+
+// packages/daisyui/components/toggle/index.js
+var toggle_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtoggle = addPrefix(object_default33, prefix);
+ addComponents({ ...prefixedtoggle });
+};
+
+// packages/daisyui/components/stack/object.js
+var object_default34 = { ".stack": { display: "inline-grid", "grid-template-columns": "3px 4px 1fr 4px 3px", "grid-template-rows": "3px 4px 1fr 4px 3px", "& > *": { height: "100%", width: "100%", "&:nth-child(n + 2)": { width: "100%", opacity: "70%" }, "&:nth-child(2)": { "z-index": 2, opacity: "90%" }, "&:nth-child(1)": { "z-index": 3, width: "100%" } }, "&, &.stack-bottom": { "> *": { "grid-column": "3 / 4", "grid-row": "3 / 6", "&:nth-child(2)": { "grid-column": "2 / 5", "grid-row": "2 / 5" }, "&:nth-child(1)": { "grid-column": "1 / 6", "grid-row": "1 / 4" } } }, "&.stack-top": { "> *": { "grid-column": "3 / 4", "grid-row": "1 / 4", "&:nth-child(2)": { "grid-column": "2 / 5", "grid-row": "2 / 5" }, "&:nth-child(1)": { "grid-column": "1 / 6", "grid-row": "3 / 6" } } }, "&.stack-start": { "> *": { "grid-column": "1 / 4", "grid-row": "3 / 4", "&:nth-child(2)": { "grid-column": "2 / 5", "grid-row": "2 / 5" }, "&:nth-child(1)": { "grid-column": "3 / 6", "grid-row": "1 / 6" } } }, "&.stack-end": { "> *": { "grid-column": "3 / 6", "grid-row": "3 / 4", "&:nth-child(2)": { "grid-column": "2 / 5", "grid-row": "2 / 5" }, "&:nth-child(1)": { "grid-column": "1 / 4", "grid-row": "1 / 6" } } } } };
+
+// packages/daisyui/components/stack/index.js
+var stack_default = ({ addComponents, prefix = "" }) => {
+ const prefixedstack = addPrefix(object_default34, prefix);
+ addComponents({ ...prefixedstack });
+};
+
+// packages/daisyui/components/navbar/object.js
+var object_default35 = { ".navbar": { display: "flex", width: "100%", "align-items": "center", padding: "0.5rem", "min-height": "4rem" }, ".navbar-start": { display: "inline-flex", "align-items": "center", width: "50%", "justify-content": "flex-start" }, ".navbar-center": { display: "inline-flex", "align-items": "center", "flex-shrink": 0 }, ".navbar-end": { display: "inline-flex", "align-items": "center", width: "50%", "justify-content": "flex-end" } };
+
+// packages/daisyui/components/navbar/index.js
+var navbar_default = ({ addComponents, prefix = "" }) => {
+ const prefixednavbar = addPrefix(object_default35, prefix);
+ addComponents({ ...prefixednavbar });
+};
+
+// packages/daisyui/components/label/object.js
+var object_default36 = { ".label": { display: "inline-flex", "align-items": "center", gap: "calc(0.25rem * 1.5)", "white-space": "nowrap", color: "color-mix(in oklab, currentColor 60%, transparent)", "&:has(input)": { cursor: "pointer" }, "&:is(.input > *, .select > *)": { display: "flex", height: "calc(100% - 0.5rem)", "align-items": "center", "padding-inline": "calc(0.25rem * 3)", "white-space": "nowrap", "font-size": "inherit", "&:first-child": { "margin-inline-start": "calc(0.25rem * -3)", "margin-inline-end": "calc(0.25rem * 3)", "border-inline-end": "var(--border) solid color-mix(in oklab, currentColor 10%, #0000)" }, "&:last-child": { "margin-inline-start": "calc(0.25rem * 3)", "margin-inline-end": "calc(0.25rem * -3)", "border-inline-start": "var(--border) solid color-mix(in oklab, currentColor 10%, #0000)" } } }, ".floating-label": { position: "relative", display: "block", input: { display: "block", "&::placeholder": { transition: "top 0.1s ease-out, translate 0.1s ease-out, scale 0.1s ease-out, opacity 0.1s ease-out" } }, textarea: { "&::placeholder": { transition: "top 0.1s ease-out, translate 0.1s ease-out, scale 0.1s ease-out, opacity 0.1s ease-out" } }, "> span": { position: "absolute", "inset-inline-start": "calc(0.25rem * 3)", "z-index": 1, "background-color": "var(--color-base-100)", "padding-inline": "calc(0.25rem * 1)", opacity: "0%", "font-size": "0.875rem", top: "calc(var(--size-field, 0.25rem) * 10 / 2)", "line-height": 1, "border-radius": "2px", "pointer-events": "none", translate: "0 -50%", transition: "top 0.1s ease-out, translate 0.1s ease-out, scale 0.1s ease-out, opacity 0.1s ease-out" }, "&:focus-within, &:not(:has(input:placeholder-shown, textarea:placeholder-shown))": { "::placeholder": { opacity: "0%", top: "0", translate: "-12.5% calc(-50% - 0.125em)", scale: "0.75", "pointer-events": "auto" }, "> span": { opacity: "100%", top: "0", translate: "-12.5% calc(-50% - 0.125em)", scale: "0.75", "pointer-events": "auto", "z-index": 2 } }, "&:has(:disabled, [disabled])": { "> span": { opacity: "0%" } }, "&:has(.input-xs, .select-xs, .textarea-xs) span": { "font-size": "0.6875rem", top: "calc(var(--size-field, 0.25rem) * 6 / 2)" }, "&:has(.input-sm, .select-sm, .textarea-sm) span": { "font-size": "0.75rem", top: "calc(var(--size-field, 0.25rem) * 8 / 2)" }, "&:has(.input-md, .select-md, .textarea-md) span": { "font-size": "0.875rem", top: "calc(var(--size-field, 0.25rem) * 10 / 2)" }, "&:has(.input-lg, .select-lg, .textarea-lg) span": { "font-size": "1.125rem", top: "calc(var(--size-field, 0.25rem) * 12 / 2)" }, "&:has(.input-xl, .select-xl, .textarea-xl) span": { "font-size": "1.375rem", top: "calc(var(--size-field, 0.25rem) * 14 / 2)" } } };
+
+// packages/daisyui/components/label/index.js
+var label_default = ({ addComponents, prefix = "" }) => {
+ const prefixedlabel = addPrefix(object_default36, prefix);
+ addComponents({ ...prefixedlabel });
+};
+
+// packages/daisyui/components/menu/object.js
+var object_default37 = { ".menu": { display: "flex", width: "fit-content", "flex-direction": "column", "flex-wrap": "wrap", padding: "calc(0.25rem * 2)", "--menu-active-fg": "var(--color-neutral-content)", "--menu-active-bg": "var(--color-neutral)", "font-size": "0.875rem", ":where(li ul)": { position: "relative", "margin-inline-start": "calc(0.25rem * 4)", "padding-inline-start": "calc(0.25rem * 2)", "white-space": "nowrap", "&:before": { position: "absolute", "inset-inline-start": "calc(0.25rem * 0)", top: "calc(0.25rem * 3)", bottom: "calc(0.25rem * 3)", "background-color": "var(--color-base-content)", opacity: "10%", width: "var(--border)", content: '""' } }, ":where(li > .menu-dropdown:not(.menu-dropdown-show))": { display: "none" }, ":where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), :where(li:not(.menu-title) > details > summary:not(.menu-title))": { display: "grid", "grid-auto-flow": "column", "align-content": "flex-start", "align-items": "center", gap: "calc(0.25rem * 2)", "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 1.5)", "text-align": "start", "transition-property": "color, background-color, box-shadow", "transition-duration": "0.2s", "transition-timing-function": "cubic-bezier(0, 0, 0.2, 1)", "grid-auto-columns": "minmax(auto, max-content) auto max-content", "text-wrap": "balance", "user-select": "none" }, ":where(li > details > summary)": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" }, "&::-webkit-details-marker": { display: "none" } }, ":where(li > details > summary), :where(li > .menu-dropdown-toggle)": { "&:after": { "justify-self": "flex-end", display: "block", height: "0.375rem", width: "0.375rem", rotate: "-135deg", translate: "0 -1px", "transition-property": "rotate, translate", "transition-duration": "0.2s", content: '""', "transform-origin": "50% 50%", "box-shadow": "2px 2px inset", "pointer-events": "none" } }, ":where(li > details[open] > summary):after, :where(li > .menu-dropdown-toggle.menu-dropdown-show):after": { rotate: "45deg", translate: "0 1px" }, ":where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title), li:not(.menu-title, .disabled) > details > summary:not(.menu-title) ):not(.menu-active, :active, .btn)": { "&.menu-focus, &:focus-visible": { cursor: "pointer", "background-color": "color-mix(in oklab, var(--color-base-content) 10%, transparent)", color: "var(--color-base-content)", "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } } }, ":where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title):not(.menu-active, :active, .btn):hover, li:not(.menu-title, .disabled) > details > summary:not(.menu-title):not(.menu-active, :active, .btn):hover )": { cursor: "pointer", "background-color": "color-mix(in oklab, var(--color-base-content) 10%, transparent)", "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" }, "box-shadow": "0 1px oklch(0% 0 0 / 0.01) inset, 0 -1px oklch(100% 0 0 / 0.01) inset" }, ":where(li:empty)": { "background-color": "var(--color-base-content)", opacity: "10%", margin: "0.5rem 1rem", height: "1px" }, ":where(li)": { position: "relative", display: "flex", "flex-shrink": 0, "flex-direction": "column", "flex-wrap": "wrap", "align-items": "stretch", ".badge": { "justify-self": "flex-end" }, "& > *:not(ul, .menu-title, details, .btn):active, & > *:not(ul, .menu-title, details, .btn).menu-active, & > details > summary:active": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" }, color: "var(--menu-active-fg)", "background-color": "var(--menu-active-bg)", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--fx-noise)", "&:not(&:active)": { "box-shadow": "0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg)" } }, "&.menu-disabled": { "pointer-events": "none", color: "color-mix(in oklab, var(--color-base-content) 20%, transparent)" } }, ".dropdown:focus-within": { ".menu-dropdown-toggle:after": { rotate: "45deg", translate: "0 1px" } }, ".dropdown-content": { "margin-top": "calc(0.25rem * 2)", padding: "calc(0.25rem * 2)", "&:before": { display: "none" } } }, ".menu-title": { "padding-inline": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 2)", color: "color-mix(in oklab, var(--color-base-content) 40%, transparent)", "font-size": "0.875rem", "font-weight": 600 }, ".menu-horizontal": { display: "inline-flex", "flex-direction": "row", "& > li:not(.menu-title) > details > ul": { position: "absolute", "margin-inline-start": "calc(0.25rem * 0)", "margin-top": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 2)", "padding-inline-end": "calc(0.25rem * 2)" }, "& > li > details > ul": { "&:before": { content: "none" } }, ":where(& > li:not(.menu-title) > details > ul)": { "border-radius": "var(--radius-box)", "background-color": "var(--color-base-100)", "box-shadow": "0 1px 3px 0 oklch(0% 0 0/0.1), 0 1px 2px -1px oklch(0% 0 0/0.1)" } }, ".menu-vertical": { display: "inline-flex", "flex-direction": "column", "& > li:not(.menu-title) > details > ul": { position: "relative", "margin-inline-start": "calc(0.25rem * 4)", "margin-top": "calc(0.25rem * 0)", "padding-block": "calc(0.25rem * 0)", "padding-inline-end": "calc(0.25rem * 0)" } }, ".menu-xs": { ":where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title))": { "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 2)", "padding-block": "calc(0.25rem * 1)", "font-size": "0.6875rem" }, ".menu-title": { "padding-inline": "calc(0.25rem * 2)", "padding-block": "calc(0.25rem * 1)" } }, ".menu-sm": { ":where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title))": { "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 2.5)", "padding-block": "calc(0.25rem * 1)", "font-size": "0.75rem" }, ".menu-title": { "padding-inline": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 2)" } }, ".menu-md": { ":where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title))": { "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 1.5)", "font-size": "0.875rem" }, ".menu-title": { "padding-inline": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 2)" } }, ".menu-lg": { ":where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title))": { "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 1.5)", "font-size": "1.125rem" }, ".menu-title": { "padding-inline": "calc(0.25rem * 6)", "padding-block": "calc(0.25rem * 3)" } }, ".menu-xl": { ":where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title))": { "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 5)", "padding-block": "calc(0.25rem * 1.5)", "font-size": "1.375rem" }, ".menu-title": { "padding-inline": "calc(0.25rem * 6)", "padding-block": "calc(0.25rem * 3)" } } };
+
+// packages/daisyui/components/menu/index.js
+var menu_default = ({ addComponents, prefix = "" }) => {
+ const prefixedmenu = addPrefix(object_default37, prefix);
+ addComponents({ ...prefixedmenu });
+};
+
+// packages/daisyui/components/toast/object.js
+var object_default38 = { ".toast": { position: "fixed", "inset-inline-start": "auto", "inset-inline-end": "calc(0.25rem * 4)", top: "auto", bottom: "calc(0.25rem * 4)", display: "flex", "flex-direction": "column", gap: "calc(0.25rem * 2)", "background-color": "transparent", translate: "var(--toast-x, 0) var(--toast-y, 0)", width: "max-content", "max-width": "calc(100vw - 2rem)", "& > *": { animation: "toast 0.25s ease-out" }, "&:where(.toast-start)": { "inset-inline-start": "calc(0.25rem * 4)", "inset-inline-end": "auto", "--toast-x": "0" }, "&:where(.toast-center)": { "inset-inline-start": "calc(1/2 * 100%)", "inset-inline-end": "calc(1/2 * 100%)", "--toast-x": "-50%" }, "&:where(.toast-end)": { "inset-inline-start": "auto", "inset-inline-end": "calc(0.25rem * 4)", "--toast-x": "0" }, "&:where(.toast-bottom)": { top: "auto", bottom: "calc(0.25rem * 4)", "--toast-y": "0" }, "&:where(.toast-middle)": { top: "calc(1/2 * 100%)", bottom: "auto", "--toast-y": "-50%" }, "&:where(.toast-top)": { top: "calc(0.25rem * 4)", bottom: "auto", "--toast-y": "0" } }, "@keyframes toast": { "0%": { scale: "0.9", opacity: 0 }, "100%": { scale: "1", opacity: 1 } } };
+
+// packages/daisyui/components/toast/index.js
+var toast_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtoast = addPrefix(object_default38, prefix);
+ addComponents({ ...prefixedtoast });
+};
+
+// packages/daisyui/components/button/object.js
+var object_default39 = { ":where(.btn)": { width: "unset" }, ".btn": { display: "inline-flex", "flex-shrink": 0, cursor: "pointer", "flex-wrap": "nowrap", "align-items": "center", "justify-content": "center", gap: "calc(0.25rem * 1.5)", "text-align": "center", "vertical-align": "middle", "outline-offset": "2px", "webkit-user-select": "none", "user-select": "none", "padding-inline": "var(--btn-p)", color: "var(--btn-fg)", "--tw-prose-links": "var(--btn-fg)", height: "var(--size)", "font-size": "var(--fontsize, 0.875rem)", "font-weight": 600, "outline-color": "var(--btn-color, var(--color-base-content))", "transition-property": "color, background-color, border-color, box-shadow", "transition-timing-function": "cubic-bezier(0, 0, 0.2, 1)", "transition-duration": "0.2s", "border-start-start-radius": "var(--join-ss, var(--radius-field))", "border-start-end-radius": "var(--join-se, var(--radius-field))", "border-end-start-radius": "var(--join-es, var(--radius-field))", "border-end-end-radius": "var(--join-ee, var(--radius-field))", "background-color": "var(--btn-bg)", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--btn-noise)", "border-width": "var(--border)", "border-style": "solid", "border-color": "var(--btn-border)", "text-shadow": "0 0.5px oklch(100% 0 0 / calc(var(--depth) * 0.15))", "touch-action": "manipulation", "box-shadow": "0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow)", "--size": "calc(var(--size-field, 0.25rem) * 10)", "--btn-bg": "var(--btn-color, var(--color-base-200))", "--btn-fg": "var(--color-base-content)", "--btn-p": "1rem", "--btn-border": "color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))", "--btn-shadow": `0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000),
+ 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)`, "--btn-noise": "var(--fx-noise)", ".prose &": { "text-decoration-line": "none" }, "@media (hover: hover)": { "&:hover": { "--btn-bg": "color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%)" } }, "&:focus-visible": { "outline-width": "2px", "outline-style": "solid", isolation: "isolate" }, "&:active:not(.btn-active)": { translate: "0 0.5px", "--btn-bg": "color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 5%)", "--btn-border": "color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%)", "--btn-shadow": "0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0)" }, "&:is(:disabled, [disabled], .btn-disabled)": { "&:not(.btn-link, .btn-ghost)": { "background-color": "color-mix(in oklab, var(--color-base-content) 10%, transparent)", "box-shadow": "none" }, "pointer-events": "none", "--btn-border": "#0000", "--btn-noise": "none", "--btn-fg": "color-mix(in oklch, var(--color-base-content) 20%, #0000)", "@media (hover: hover)": { "&:hover": { "pointer-events": "none", "background-color": "color-mix(in oklab, var(--color-neutral) 20%, transparent)", "--btn-border": "#0000", "--btn-fg": "color-mix(in oklch, var(--color-base-content) 20%, #0000)" } } }, '&:is(input[type="checkbox"], input[type="radio"])': { appearance: "none", "&::after": { content: "attr(aria-label)" } }, "&:where(input:checked:not(.filter .btn))": { "--btn-color": "var(--color-primary)", "--btn-fg": "var(--color-primary-content)", isolation: "isolate" } }, ".btn-active": { "--btn-bg": "color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%)", "--btn-shadow": "0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0)", isolation: "isolate" }, ".btn-primary": { "--btn-color": "var(--color-primary)", "--btn-fg": "var(--color-primary-content)" }, ".btn-secondary": { "--btn-color": "var(--color-secondary)", "--btn-fg": "var(--color-secondary-content)" }, ".btn-accent": { "--btn-color": "var(--color-accent)", "--btn-fg": "var(--color-accent-content)" }, ".btn-neutral": { "--btn-color": "var(--color-neutral)", "--btn-fg": "var(--color-neutral-content)" }, ".btn-info": { "--btn-color": "var(--color-info)", "--btn-fg": "var(--color-info-content)" }, ".btn-success": { "--btn-color": "var(--color-success)", "--btn-fg": "var(--color-success-content)" }, ".btn-warning": { "--btn-color": "var(--color-warning)", "--btn-fg": "var(--color-warning-content)" }, ".btn-error": { "--btn-color": "var(--color-error)", "--btn-fg": "var(--color-error-content)" }, ".btn-ghost": { "&:not(.btn-active, :hover, :active:focus, :focus-visible)": { "--btn-shadow": '""', "--btn-bg": "#0000", "--btn-border": "#0000", "--btn-noise": "none", "&:not(:disabled, [disabled], .btn-disabled)": { "outline-color": "currentColor", "--btn-fg": "currentColor" } } }, ".btn-link": { "text-decoration-line": "underline", "outline-color": "currentColor", "--btn-border": "#0000", "--btn-bg": "#0000", "--btn-fg": "var(--color-primary)", "--btn-noise": "none", "--btn-shadow": '""', "&:is(.btn-active, :hover, :active:focus, :focus-visible)": { "text-decoration-line": "underline", "--btn-border": "#0000", "--btn-bg": "#0000" } }, ".btn-outline": { "&:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked )": { "--btn-shadow": '""', "--btn-bg": "#0000", "--btn-fg": "var(--btn-color)", "--btn-border": "var(--btn-color)", "--btn-noise": "none" }, "@media (hover: none)": { "&:hover:not( .btn-active, :active, :focus-visible, :disabled, [disabled], .btn-disabled, :checked )": { "--btn-shadow": '""', "--btn-bg": "#0000", "--btn-fg": "var(--btn-color)", "--btn-border": "var(--btn-color)", "--btn-noise": "none" } } }, ".btn-dash": { "&:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked )": { "--btn-shadow": '""', "border-style": "dashed", "--btn-bg": "#0000", "--btn-fg": "var(--btn-color)", "--btn-border": "var(--btn-color)", "--btn-noise": "none" }, "@media (hover: none)": { "&:hover:not( .btn-active, :active, :focus-visible, :disabled, [disabled], .btn-disabled, :checked )": { "--btn-shadow": '""', "border-style": "dashed", "--btn-bg": "#0000", "--btn-fg": "var(--btn-color)", "--btn-border": "var(--btn-color)", "--btn-noise": "none" } } }, ".btn-soft": { "&:not(.btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled)": { "--btn-shadow": '""', "--btn-fg": "var(--btn-color, var(--color-base-content))", "--btn-bg": `color-mix(
+ in oklab,
+ var(--btn-color, var(--color-base-content)) 8%,
+ var(--color-base-100)
+ )`, "--btn-border": `color-mix(
+ in oklab,
+ var(--btn-color, var(--color-base-content)) 10%,
+ var(--color-base-100)
+ )`, "--btn-noise": "none" }, "@media (hover: none)": { "&:hover:not(.btn-active, :active, :focus-visible, :disabled, [disabled], .btn-disabled)": { "--btn-shadow": '""', "--btn-fg": "var(--btn-color, var(--color-base-content))", "--btn-bg": `color-mix(
+ in oklab,
+ var(--btn-color, var(--color-base-content)) 8%,
+ var(--color-base-100)
+ )`, "--btn-border": `color-mix(
+ in oklab,
+ var(--btn-color, var(--color-base-content)) 10%,
+ var(--color-base-100)
+ )`, "--btn-noise": "none" } } }, ".btn-xs": { "--fontsize": "0.6875rem", "--btn-p": "0.5rem", "--size": "calc(var(--size-field, 0.25rem) * 6)" }, ".btn-sm": { "--fontsize": "0.75rem", "--btn-p": "0.75rem", "--size": "calc(var(--size-field, 0.25rem) * 8)" }, ".btn-md": { "--fontsize": "0.875rem", "--btn-p": "1rem", "--size": "calc(var(--size-field, 0.25rem) * 10)" }, ".btn-lg": { "--fontsize": "1.125rem", "--btn-p": "1.25rem", "--size": "calc(var(--size-field, 0.25rem) * 12)" }, ".btn-xl": { "--fontsize": "1.375rem", "--btn-p": "1.5rem", "--size": "calc(var(--size-field, 0.25rem) * 14)" }, ".btn-square": { "padding-inline": "calc(0.25rem * 0)", width: "var(--size)", height: "var(--size)" }, ".btn-circle": { "border-radius": "calc(infinity * 1px)", "padding-inline": "calc(0.25rem * 0)", width: "var(--size)", height: "var(--size)" }, ".btn-wide": { width: "100%", "max-width": "calc(0.25rem * 64)" }, ".btn-block": { width: "100%" } };
+
+// packages/daisyui/components/button/index.js
+var button_default = ({ addComponents, prefix = "" }) => {
+ const prefixedbutton = addPrefix(object_default39, prefix);
+ addComponents({ ...prefixedbutton });
+};
+
+// packages/daisyui/components/list/object.js
+var object_default40 = { ".list": { display: "flex", "flex-direction": "column", "font-size": "0.875rem", ":where(.list-row)": { "--list-grid-cols": "minmax(0, auto) 1fr", position: "relative", display: "grid", "grid-auto-flow": "column", gap: "calc(0.25rem * 4)", "border-radius": "var(--radius-box)", padding: "calc(0.25rem * 4)", "word-break": "break-word", "grid-template-columns": "var(--list-grid-cols)", "&:has(.list-col-grow:nth-child(1))": { "--list-grid-cols": "1fr" }, "&:has(.list-col-grow:nth-child(2))": { "--list-grid-cols": "minmax(0, auto) 1fr" }, "&:has(.list-col-grow:nth-child(3))": { "--list-grid-cols": "minmax(0, auto) minmax(0, auto) 1fr" }, "&:has(.list-col-grow:nth-child(4))": { "--list-grid-cols": "minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr" }, "&:has(.list-col-grow:nth-child(5))": { "--list-grid-cols": "minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr" }, "&:has(.list-col-grow:nth-child(6))": { "--list-grid-cols": `minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto)
+ minmax(0, auto) 1fr` }, ":not(.list-col-wrap)": { "grid-row-start": "1" } }, "& > :not(:last-child)": { "&.list-row, .list-row": { "&:after": { content: '""', "border-bottom": "var(--border) solid", "inset-inline": "var(--radius-box)", position: "absolute", bottom: "calc(0.25rem * 0)", "border-color": "color-mix(in oklab, var(--color-base-content) 5%, transparent)" } } } }, ".list-col-wrap": { "grid-row-start": "2" } };
+
+// packages/daisyui/components/list/index.js
+var list_default = ({ addComponents, prefix = "" }) => {
+ const prefixedlist = addPrefix(object_default40, prefix);
+ addComponents({ ...prefixedlist });
+};
+
+// packages/daisyui/components/mockup/object.js
+var object_default41 = { ".mockup-code": { position: "relative", overflow: "hidden", "overflow-x": "auto", "border-radius": "var(--radius-box)", "background-color": "var(--color-neutral)", "padding-block": "calc(0.25rem * 5)", color: "var(--color-neutral-content)", "font-size": "0.875rem", direction: "ltr", "&:before": { content: '""', "margin-bottom": "calc(0.25rem * 4)", display: "block", height: "calc(0.25rem * 3)", width: "calc(0.25rem * 3)", "border-radius": "calc(infinity * 1px)", opacity: "30%", "box-shadow": "1.4em 0, 2.8em 0, 4.2em 0" }, pre: { "padding-right": "calc(0.25rem * 5)", "&:before": { content: '""', "margin-right": "2ch" }, "&[data-prefix]": { "&:before": { content: "attr(data-prefix)", display: "inline-block", width: "calc(0.25rem * 8)", "text-align": "right", opacity: "50%" } } } }, ".mockup-window": { position: "relative", display: "flex", "flex-direction": "column", overflow: "hidden", "overflow-x": "auto", "border-radius": "var(--radius-box)", "padding-top": "calc(0.25rem * 5)", "&:before": { content: '""', "margin-bottom": "calc(0.25rem * 4)", display: "block", "aspect-ratio": "1 / 1", height: "calc(0.25rem * 3)", "flex-shrink": 0, "align-self": "flex-start", "border-radius": "calc(infinity * 1px)", opacity: "30%", "box-shadow": "1.4em 0, 2.8em 0, 4.2em 0" }, '[dir="rtl"] &:before': { "align-self": "flex-end" }, "pre[data-prefix]": { "&:before": { content: "attr(data-prefix)", display: "inline-block", "text-align": "right" } } }, ".mockup-browser": { position: "relative", overflow: "hidden", "overflow-x": "auto", "border-radius": "var(--radius-box)", "pre[data-prefix]": { "&:before": { content: "attr(data-prefix)", display: "inline-block", "text-align": "right" } }, ".mockup-browser-toolbar": { "margin-block": "calc(0.25rem * 3)", display: "inline-flex", width: "100%", "align-items": "center", "padding-right": "1.4em", '&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)': { "flex-direction": "row-reverse" }, "&:before": { content: '""', "margin-right": "4.8rem", display: "inline-block", "aspect-ratio": "1 / 1", height: "calc(0.25rem * 3)", "border-radius": "calc(infinity * 1px)", opacity: "30%", "box-shadow": "1.4em 0, 2.8em 0, 4.2em 0" }, ".input": { "margin-inline": "auto", display: "flex", height: "100%", "align-items": "center", gap: "calc(0.25rem * 2)", overflow: "hidden", "background-color": "var(--color-base-200)", "text-overflow": "ellipsis", "white-space": "nowrap", "font-size": "0.75rem", direction: "ltr", "&:before": { content: '""', width: "calc(0.25rem * 4)", height: "calc(0.25rem * 4)", opacity: "30%", "background-image": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor' class='size-4'%3E%3Cpath fill-rule='evenodd' d='M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A")` } } } }, ".mockup-phone": { display: "inline-grid", "justify-items": "center", border: "6px solid #6b6b6b", "border-radius": "65px", "background-color": "#000", padding: "11px", overflow: "hidden" }, ".mockup-phone-camera": { "grid-column": "1/1", "grid-row": "1/1", background: "#000", height: "32px", width: "126px", "border-radius": "17px", "z-index": 1, "margin-top": "6px" }, ".mockup-phone-display": { "grid-column": "1/1", "grid-row": "1/1", overflow: "hidden", "border-radius": "49px", width: "390px", height: "845px" } };
+
+// packages/daisyui/components/mockup/index.js
+var mockup_default = ({ addComponents, prefix = "" }) => {
+ const prefixedmockup = addPrefix(object_default41, prefix);
+ addComponents({ ...prefixedmockup });
+};
+
+// packages/daisyui/components/calendar/object.js
+var object_default42 = { ".cally": { "font-size": "0.7rem", "&::part(container)": { padding: "0.5rem 1rem", "user-select": "none" }, "::part(th)": { "font-weight": "normal", "block-size": "auto" }, "&::part(header)": { direction: "ltr" }, "::part(head)": { opacity: 0.5, "font-size": "0.7rem" }, "&::part(button)": { "border-radius": "var(--radius-field)", border: "none", padding: "0.5rem", background: "#0000" }, "&::part(button):hover": { background: "var(--color-base-200)" }, "::part(day)": { "border-radius": "var(--radius-field)", "font-size": "0.7rem" }, "::part(button day today)": { background: "var(--color-primary)", color: "var(--color-primary-content)" }, "::part(selected)": { color: "var(--color-base-100)", background: "var(--color-base-content)", "border-radius": "var(--radius-field)" }, "::part(range-inner)": { "border-radius": "0" }, "::part(range-start)": { "border-start-end-radius": "0", "border-end-end-radius": "0" }, "::part(range-end)": { "border-start-start-radius": "0", "border-end-start-radius": "0" }, "::part(range-start range-end)": { "border-radius": "var(--radius-field)" }, "calendar-month": { width: "100%" } }, ".react-day-picker": { "user-select": "none", "background-color": "var(--color-base-100)", "border-radius": "var(--radius-box)", border: "var(--border) solid var(--color-base-200)", "font-size": "0.75rem", display: "inline-block", position: "relative", overflow: "clip", '&[dir="rtl"]': { ".rdp-nav": { ".rdp-chevron": { "transform-origin": "50%", transform: "rotate(180deg)" } } }, "*": { "box-sizing": "border-box" }, ".rdp-day": { width: "2.25rem", height: "2.25rem", "text-align": "center" }, ".rdp-day_button": { cursor: "pointer", font: "inherit", color: "inherit", width: "2.25rem", height: "2.25rem", border: "2px solid #0000", "border-radius": "var(--radius-field)", background: "0 0", "justify-content": "center", "align-items": "center", margin: "0", padding: "0", display: "flex", "&:disabled": { cursor: "revert" }, "&:hover": { "background-color": "var(--color-base-200)" } }, ".rdp-caption_label": { "z-index": 1, "white-space": "nowrap", border: "0", "align-items": "center", display: "inline-flex", position: "relative" }, ".rdp-button_next": { "border-radius": "var(--radius-field)", "&:hover": { "background-color": "var(--color-base-200)" } }, ".rdp-button_previous": { "border-radius": "var(--radius-field)", "&:hover": { "background-color": "var(--color-base-200)" } }, ".rdp-button_next, .rdp-button_previous": { cursor: "pointer", font: "inherit", color: "inherit", appearance: "none", width: "2.25rem", height: "2.25rem", background: "0 0", border: "none", "justify-content": "center", "align-items": "center", margin: "0", padding: "0", display: "inline-flex", position: "relative", "&:disabled": { cursor: "revert", opacity: 0.5 } }, ".rdp-chevron": { fill: "var(--color-base-content)", width: "1rem", height: "1rem", display: "inline-block" }, ".rdp-dropdowns": { "align-items": "center", gap: "0.5rem", display: "inline-flex", position: "relative" }, ".rdp-dropdown": { "z-index": 2, opacity: 0, appearance: "none", cursor: "inherit", "line-height": "inherit", border: "none", width: "100%", margin: "0", padding: "0", position: "absolute", "inset-block": "0", "inset-inline-start": "0", "&:focus-visible": { "~ .rdp-caption_label": { outline: ["5px auto highlight", "5px auto -webkit-focus-ring-color"] } } }, ".rdp-dropdown_root": { "align-items": "center", display: "inline-flex", position: "relative", '&[data-disabled="true"]': { ".rdp-chevron": { opacity: 0.5 } } }, ".rdp-month_caption": { height: "2.75rem", "font-size": "0.75rem", "font-weight": "inherit", "place-content": "center", display: "flex" }, ".rdp-months": { gap: "2rem", "flex-wrap": "wrap", "max-width": "fit-content", padding: "0.5rem", display: "flex", position: "relative" }, ".rdp-month_grid": { "border-collapse": "collapse" }, ".rdp-nav": { height: "2.75rem", "inset-block-start": "0", "inset-inline-end": "0", "justify-content": "space-between", "align-items": "center", width: "100%", "padding-inline": "0.5rem", display: "flex", position: "absolute", top: "0.25rem" }, ".rdp-weekday": { opacity: 0.6, padding: "0.5rem 0rem", "text-align": "center", "font-size": "smaller", "font-weight": 500 }, ".rdp-week_number": { opacity: 0.6, height: "2.25rem", width: "2.25rem", border: "none", "border-radius": "100%", "text-align": "center", "font-size": "small", "font-weight": 400 }, ".rdp-today:not(.rdp-outside)": { ".rdp-day_button": { background: "var(--color-primary)", color: "var(--color-primary-content)" } }, ".rdp-selected": { "font-weight": "inherit", "font-size": "0.75rem", ".rdp-day_button": { color: "var(--color-base-100)", "background-color": "var(--color-base-content)", "border-radius": "var(--radius-field)", border: "none", "&:hover": { "background-color": "var(--color-base-content)" } } }, ".rdp-outside": { opacity: 0.75 }, ".rdp-disabled": { opacity: 0.5 }, ".rdp-hidden": { visibility: "hidden", color: "var(--color-base-content)" }, ".rdp-range_start": { ".rdp-day_button": { "border-radius": "var(--radius-field) 0 0 var(--radius-field)" } }, ".rdp-range_start .rdp-day_button": { "background-color": "var(--color-base-content)", color: "var(--color-base-content)" }, ".rdp-range_middle": { "background-color": "var(--color-base-200)" }, ".rdp-range_middle .rdp-day_button": { border: "unset", "border-radius": "unset", color: "inherit" }, ".rdp-range_end": { color: "var(--color-base-content)", ".rdp-day_button": { "border-radius": "0 var(--radius-field) var(--radius-field) 0" } }, ".rdp-range_end .rdp-day_button": { color: "var(--color-base-content)", "background-color": "var(--color-base-content)" }, ".rdp-range_start.rdp-range_end": { background: "revert" }, ".rdp-focusable": { cursor: "pointer" }, ".rdp-footer": { "border-top": "var(--border) solid var(--color-base-200)", padding: "0.5rem" } }, ".pika-single": { "&:is(div)": { "user-select": "none", "font-size": "0.75rem", "z-index": 999, display: "inline-block", position: "relative", color: "var(--color-base-content)", "background-color": "var(--color-base-100)", "border-radius": "var(--radius-box)", border: "var(--border) solid var(--color-base-200)", padding: "0.5rem", "&:before, &:after": { content: '""', display: "table" }, "&:after": { clear: "both" }, "&.is-hidden": { display: "none" }, "&.is-bound": { position: "absolute" }, ".pika-lendar": { "css-float": "left" }, ".pika-title": { position: "relative", "text-align": "center", select: { cursor: "pointer", position: "absolute", "z-index": 999, margin: "0", left: "0", top: "5px", opacity: 0 } }, ".pika-label": { display: "inline-block", position: "relative", "z-index": 999, overflow: "hidden", margin: "0", padding: "5px 3px", "background-color": "var(--color-base-100)" }, ".pika-prev, .pika-next": { display: "block", cursor: "pointer", position: "absolute", top: "0", outline: "none", border: "0", width: "2.25rem", height: "2.25rem", color: "#0000", "font-size": "1.2em", "border-radius": "var(--radius-field)", "&:hover": { "background-color": "var(--color-base-200)" }, "&.is-disabled": { cursor: "default", opacity: 0.2 }, "&:before": { display: "inline-block", width: "2.25rem", height: "2.25rem", "line-height": 2.25, color: "var(--color-base-content)" } }, ".pika-prev": { left: "0", "&:before": { content: '"‹"' } }, ".pika-next": { right: "0", "&:before": { content: '"›"' } }, ".pika-select": { display: "inline-block" }, ".pika-table": { width: "100%", "border-collapse": "collapse", "border-spacing": "0", border: "0", "th, td": { padding: "0" }, th: { opacity: 0.6, "text-align": "center", width: "2.25rem", height: "2.25rem" } }, ".pika-button": { cursor: "pointer", display: "block", outline: "none", border: "0", margin: "0", width: "2.25rem", height: "2.25rem", padding: "5px", "text-align": ["right", "center"] }, ".pika-week": { color: "var(--color-base-content)" }, ".is-today": { ".pika-button": { background: "var(--color-primary)", color: "var(--color-primary-content)" } }, ".is-selected, .has-event": { ".pika-button": { "&, &:hover": { color: "var(--color-base-100)", "background-color": "var(--color-base-content)", "border-radius": "var(--radius-field)" } } }, ".has-event": { ".pika-button": { background: "var(--color-base-primary)" } }, ".is-disabled, .is-inrange": { ".pika-button": { background: "var(--color-base-primary)" } }, ".is-startrange": { ".pika-button": { color: "var(--color-base-100)", background: "var(--color-base-content)", "border-radius": "var(--radius-field)" } }, ".is-endrange": { ".pika-button": { color: "var(--color-base-100)", background: "var(--color-base-content)", "border-radius": "var(--radius-field)" } }, ".is-disabled": { ".pika-button": { "pointer-events": "none", cursor: "default", color: "var(--color-base-content)", opacity: 0.3 } }, ".is-outside-current-month": { ".pika-button": { color: "var(--color-base-content)", opacity: 0.3 } }, ".is-selection-disabled": { "pointer-events": "none", cursor: "default" }, ".pika-button:hover, .pika-row.pick-whole-week:hover .pika-button": { color: "var(--color-base-content)", "background-color": "var(--color-base-200)", "border-radius": "var(--radius-field)" }, ".pika-table abbr": { "text-decoration": "none", "font-weight": "normal" } } } };
+
+// packages/daisyui/components/calendar/index.js
+var calendar_default = ({ addComponents, prefix = "" }) => {
+ const prefixedcalendar = addPrefix(object_default42, prefix);
+ addComponents({ ...prefixedcalendar });
+};
+
+// packages/daisyui/components/indicator/object.js
+var object_default43 = { ".indicator": { position: "relative", display: "inline-flex", width: "max-content", ":where(.indicator-item)": { "z-index": 1, position: "absolute", "white-space": "nowrap", top: "var(--inidicator-t, 0)", bottom: "var(--inidicator-b, auto)", left: "var(--inidicator-s, auto)", right: "var(--inidicator-e, 0)", translate: "var(--inidicator-x, 50%) var(--indicator-y, -50%)" } }, ".indicator-start": { "--inidicator-s": "0", "--inidicator-e": "auto", "--inidicator-x": "-50%" }, ".indicator-center": { "--inidicator-s": "50%", "--inidicator-e": "50%", "--inidicator-x": "-50%", '[dir="rtl"] &': { "--inidicator-x": "50%" } }, ".indicator-end": { "--inidicator-s": "auto", "--inidicator-e": "0", "--inidicator-x": "50%" }, ".indicator-bottom": { "--inidicator-t": "auto", "--inidicator-b": "0", "--indicator-y": "50%" }, ".indicator-middle": { "--inidicator-t": "50%", "--inidicator-b": "50%", "--indicator-y": "-50%" }, ".indicator-top": { "--inidicator-t": "0", "--inidicator-b": "auto", "--indicator-y": "-50%" } };
+
+// packages/daisyui/components/indicator/index.js
+var indicator_default = ({ addComponents, prefix = "" }) => {
+ const prefixedindicator = addPrefix(object_default43, prefix);
+ addComponents({ ...prefixedindicator });
+};
+
+// packages/daisyui/components/rating/object.js
+var object_default44 = { ".rating": { position: "relative", display: "inline-flex", "vertical-align": "middle", "& input": { border: "none", appearance: "none" }, ":where(*)": { animation: "rating 0.25s ease-out", height: "calc(0.25rem * 6)", width: "calc(0.25rem * 6)", "border-radius": "0", "background-color": "var(--color-base-content)", opacity: "20%", "&:is(input)": { cursor: "pointer" } }, "& .rating-hidden": { width: "calc(0.25rem * 2)", "background-color": "transparent" }, 'input[type="radio"]:checked': { "background-image": "none" }, "*": { '&:checked, &[aria-checked="true"], &[aria-current="true"], &:has(~ *:checked, ~ *[aria-checked="true"], ~ *[aria-current="true"])': { opacity: "100%" }, "&:focus-visible": { transition: "scale 0.2s ease-out", scale: "1.1" } }, "& *:active:focus": { animation: "none", scale: "1.1" }, "&.rating-xs :where(*:not(.rating-hidden))": { width: "calc(0.25rem * 4)", height: "calc(0.25rem * 4)" }, "&.rating-sm :where(*:not(.rating-hidden))": { width: "calc(0.25rem * 5)", height: "calc(0.25rem * 5)" }, "&.rating-md :where(*:not(.rating-hidden))": { width: "calc(0.25rem * 6)", height: "calc(0.25rem * 6)" }, "&.rating-lg :where(*:not(.rating-hidden))": { width: "calc(0.25rem * 7)", height: "calc(0.25rem * 7)" }, "&.rating-xl :where(*:not(.rating-hidden))": { width: "calc(0.25rem * 8)", height: "calc(0.25rem * 8)" } }, ".rating-half": { ":where(*:not(.rating-hidden))": { width: "calc(0.25rem * 3)" }, "&.rating-xs *:not(.rating-hidden)": { width: "calc(0.25rem * 2)" }, "&.rating-sm *:not(.rating-hidden)": { width: "calc(0.25rem * 2.5)" }, "&.rating-md *:not(.rating-hidden)": { width: "calc(0.25rem * 3)" }, "&.rating-lg *:not(.rating-hidden)": { width: ".875rem" }, "&.rating-xl *:not(.rating-hidden)": { width: "calc(0.25rem * 4)" } }, "@keyframes rating": { "0%, 40%": { scale: "1.1", filter: "brightness(1.05) contrast(1.05)" } } };
+
+// packages/daisyui/components/rating/index.js
+var rating_default = ({ addComponents, prefix = "" }) => {
+ const prefixedrating = addPrefix(object_default44, prefix);
+ addComponents({ ...prefixedrating });
+};
+
+// packages/daisyui/components/tab/object.js
+var object_default45 = { ".tabs": { display: "flex", "flex-wrap": "wrap", "--tabs-height": "auto", "--tabs-direction": "row", height: "var(--tabs-height)", "flex-direction": "var(--tabs-direction)" }, ".tab": { position: "relative", display: "inline-flex", cursor: "pointer", appearance: "none", "flex-wrap": "wrap", "align-items": "center", "justify-content": "center", "text-align": "center", "webkit-user-select": "none", "user-select": "none", "&:hover": { "@media (hover: hover)": { color: "var(--color-base-content)" } }, "--tab-p": "1rem", "--tab-bg": "var(--color-base-100)", "--tab-border-color": "var(--color-base-300)", "--tab-radius-ss": "0", "--tab-radius-se": "0", "--tab-radius-es": "0", "--tab-radius-ee": "0", "--tab-order": "0", "--tab-radius-min": "calc(0.75rem - var(--border))", "border-color": "#0000", order: "var(--tab-order)", height: "calc(var(--size-field, 0.25rem) * 10)", "font-size": "0.875rem", "padding-inline-start": "var(--tab-p)", "padding-inline-end": "var(--tab-p)", '&:is(input[type="radio"])': { "min-width": "fit-content", "&:after": { content: "attr(aria-label)" } }, "&:is(label)": { position: "relative", input: { position: "absolute", inset: "calc(0.25rem * 0)", cursor: "pointer", appearance: "none", opacity: "0%" } }, '&:checked, &:is(label:has(:checked)), &:is(.tab-active, [aria-selected="true"])': { "& + .tab-content": { display: "block", height: "100%" } }, '&:not(:checked, label:has(:checked), :hover, .tab-active, [aria-selected="true"])': { color: "color-mix(in oklab, var(--color-base-content) 50%, transparent)" }, "&:not(input):empty": { "flex-grow": 1, cursor: "default" }, "&:focus": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, "&:focus-visible, &:is(label:has(:checked:focus-visible))": { outline: "2px solid currentColor", "outline-offset": "-5px" }, "&[disabled]": { "pointer-events": "none", opacity: "40%" } }, ".tab-disabled": { "pointer-events": "none", opacity: "40%" }, ".tabs-border": { ".tab": { "--tab-border-color": "#0000 #0000 var(--tab-border-color) #0000", position: "relative", "border-radius": "var(--radius-field)", "&:before": { "--tw-content": '""', content: "var(--tw-content)", "background-color": "var(--tab-border-color)", transition: "background-color 0.2s ease", width: "80%", height: "3px", "border-radius": "var(--radius-field)", bottom: "0", left: "10%", position: "absolute" }, '&:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked), &:is(label:has(:checked))': { "&:before": { "--tab-border-color": "currentColor", "border-top": "3px solid" } } } }, ".tabs-lift": { "--tabs-height": "auto", "--tabs-direction": "row", "> .tab": { "--tab-border": "0 0 var(--border) 0", "--tab-radius-ss": "min(var(--radius-field), var(--tab-radius-min))", "--tab-radius-se": "min(var(--radius-field), var(--tab-radius-min))", "--tab-radius-es": "0", "--tab-radius-ee": "0", "--tab-paddings": "var(--border) var(--tab-p) 0 var(--tab-p)", "--tab-border-colors": "#0000 #0000 var(--tab-border-color) #0000", "--tab-corner-width": "calc(100% + min(var(--radius-field), var(--tab-radius-min)) * 2)", "--tab-corner-height": "min(var(--radius-field), var(--tab-radius-min))", "--tab-corner-position": "top left, top right", "border-width": "var(--tab-border)", "border-start-start-radius": "var(--tab-radius-ss)", "border-start-end-radius": "var(--tab-radius-se)", "border-end-start-radius": "var(--tab-radius-es)", "border-end-end-radius": "var(--tab-radius-ee)", padding: "var(--tab-paddings)", "border-color": "var(--tab-border-colors)", '&:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked, label:has(:checked))': { "--tab-border": "var(--border) var(--border) 0 var(--border)", "--tab-border-colors": `var(--tab-border-color) var(--tab-border-color) #0000
+ var(--tab-border-color)`, "--tab-paddings": `0 calc(var(--tab-p) - var(--border)) var(--border)
+ calc(var(--tab-p) - var(--border))`, "--tab-inset": "auto auto 0 auto", "--tab-grad": "calc(69% - var(--border))", "--radius-start": `radial-gradient(
+ circle at top left,
+ #0000 var(--tab-grad),
+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
+ )`, "--radius-end": `radial-gradient(
+ circle at top right,
+ #0000 var(--tab-grad),
+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
+ )`, "background-color": "var(--tab-bg)", "&:before": { "z-index": 1, content: '""', display: "block", position: "absolute", width: "var(--tab-corner-width)", height: "var(--tab-corner-height)", "background-position": "var(--tab-corner-position)", "background-image": "var(--radius-start), var(--radius-end)", "background-size": "min(var(--radius-field), var(--tab-radius-min)) min(var(--radius-field), var(--tab-radius-min))", "background-repeat": "no-repeat", inset: "var(--tab-inset)" }, "&:first-child:before": { "--radius-start": "none" }, '[dir="rtl"] &:first-child:before': { transform: "rotateY(180deg)" }, "&:last-child:before": { "--radius-end": "none" }, '[dir="rtl"] &:last-child:before': { transform: "rotateY(180deg)" } } }, "&:has(.tab-content)": { "> .tab:first-child": { '&:not(.tab-active, [aria-selected="true"])': { "--tab-border-colors": `var(--tab-border-color) var(--tab-border-color) #0000
+ var(--tab-border-color)` } } }, ".tab-content": { "--tabcontent-margin": "calc(-1 * var(--border)) 0 0 0", "--tabcontent-radius-ss": "0", "--tabcontent-radius-se": "var(--radius-box)", "--tabcontent-radius-es": "var(--radius-box)", "--tabcontent-radius-ee": "var(--radius-box)" }, ':checked, label:has(:checked), :is(.tab-active, [aria-selected="true"])': { "& + .tab-content": { "&:nth-child(1), &:nth-child(n + 3)": { "--tabcontent-radius-ss": "var(--radius-box)" } } } }, ".tabs-top": { "--tabs-height": "auto", "--tabs-direction": "row", ".tab": { "--tab-order": "0", "--tab-border": "0 0 var(--border) 0", "--tab-radius-ss": "min(var(--radius-field), var(--tab-radius-min))", "--tab-radius-se": "min(var(--radius-field), var(--tab-radius-min))", "--tab-radius-es": "0", "--tab-radius-ee": "0", "--tab-paddings": "var(--border) var(--tab-p) 0 var(--tab-p)", "--tab-border-colors": "#0000 #0000 var(--tab-border-color) #0000", "--tab-corner-width": "calc(100% + min(var(--radius-field), var(--tab-radius-min)) * 2)", "--tab-corner-height": "min(var(--radius-field), var(--tab-radius-min))", "--tab-corner-position": "top left, top right", '&:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked), &:is(label:has(:checked))': { "--tab-border": "var(--border) var(--border) 0 var(--border)", "--tab-border-colors": `var(--tab-border-color) var(--tab-border-color) #0000
+ var(--tab-border-color)`, "--tab-paddings": `0 calc(var(--tab-p) - var(--border)) var(--border)
+ calc(var(--tab-p) - var(--border))`, "--tab-inset": "auto auto 0 auto", "--radius-start": `radial-gradient(
+ circle at top left,
+ #0000 var(--tab-grad),
+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
+ )`, "--radius-end": `radial-gradient(
+ circle at top right,
+ #0000 var(--tab-grad),
+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
+ )` } }, "&:has(.tab-content)": { "> .tab:first-child": { '&:not(.tab-active, [aria-selected="true"])': { "--tab-border-colors": `var(--tab-border-color) var(--tab-border-color) #0000
+ var(--tab-border-color)` } } }, ".tab-content": { "--tabcontent-order": "1", "--tabcontent-margin": "calc(-1 * var(--border)) 0 0 0", "--tabcontent-radius-ss": "0", "--tabcontent-radius-se": "var(--radius-box)", "--tabcontent-radius-es": "var(--radius-box)", "--tabcontent-radius-ee": "var(--radius-box)" }, ':checked, label:has(:checked), :is(.tab-active, [aria-selected="true"])': { "& + .tab-content": { "&:nth-child(1), &:nth-child(n + 3)": { "--tabcontent-radius-ss": "var(--radius-box)" } } } }, ".tabs-bottom": { "--tabs-height": "auto", "--tabs-direction": "row", ".tab": { "--tab-order": "1", "--tab-border": "var(--border) 0 0 0", "--tab-radius-ss": "0", "--tab-radius-se": "0", "--tab-radius-es": "min(var(--radius-field), var(--tab-radius-min))", "--tab-radius-ee": "min(var(--radius-field), var(--tab-radius-min))", "--tab-border-colors": "var(--tab-border-color) #0000 #0000 #0000", "--tab-paddings": "0 var(--tab-p) var(--border) var(--tab-p)", "--tab-corner-width": "calc(100% + min(var(--radius-field), var(--tab-radius-min)) * 2)", "--tab-corner-height": "min(var(--radius-field), var(--tab-radius-min))", "--tab-corner-position": "top left, top right", '&:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked), &:is(label:has(:checked))': { "--tab-border": "0 var(--border) var(--border) var(--border)", "--tab-border-colors": `#0000 var(--tab-border-color) var(--tab-border-color)
+ var(--tab-border-color)`, "--tab-paddings": `var(--border) calc(var(--tab-p) - var(--border)) 0
+ calc(var(--tab-p) - var(--border))`, "--tab-inset": "0 auto auto auto", "--radius-start": `radial-gradient(
+ circle at bottom left,
+ #0000 var(--tab-grad),
+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
+ )`, "--radius-end": `radial-gradient(
+ circle at bottom right,
+ #0000 var(--tab-grad),
+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
+ )` } }, "&:has(.tab-content)": { "> .tab:first-child": { '&:not(.tab-active, [aria-selected="true"])': { "--tab-border-colors": `#0000 var(--tab-border-color) var(--tab-border-color)
+ var(--tab-border-color)` } } }, ".tab-content": { "--tabcontent-order": "0", "--tabcontent-margin": "0 0 calc(-1 * var(--border)) 0", "--tabcontent-radius-ss": "var(--radius-box)", "--tabcontent-radius-se": "var(--radius-box)", "--tabcontent-radius-es": "0", "--tabcontent-radius-ee": "var(--radius-box)" }, '> :checked, > :is(label:has(:checked)), > :is(.tab-active, [aria-selected="true"])': { "& + .tab-content:not(:nth-child(2))": { "--tabcontent-radius-es": "var(--radius-box)" } } }, ".tabs-box": { "background-color": "var(--color-base-200)", padding: "calc(0.25rem * 1)", "--tabs-box-radius": "calc(var(--radius-field) + var(--radius-field) + var(--radius-field))", "border-radius": "calc(var(--radius-field) + min(0.25rem, var(--tabs-box-radius)))", "box-shadow": "0 -0.5px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0.5px oklch(0% 0 0 / calc(var(--depth) * 0.05)) inset", ".tab": { "border-radius": "var(--radius-field)", "border-style": "none", "&:focus-visible, &:is(label:has(:checked:focus-visible))": { "outline-offset": "2px" } }, '> :is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), > :is(input:checked), > :is(label:has(:checked))': { "background-color": "var(--tab-bg, var(--color-base-100))", "box-shadow": "0 1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px 1px -1px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 50%), #0000), 0 1px 6px -4px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 100%), #0000)", "@media (forced-colors: active)": { border: "1px solid" } } }, ".tab-content": { order: [1, "var(--tabcontent-order)"], display: "none", "border-color": "transparent", "--tabcontent-radius-ss": "0", "--tabcontent-radius-se": "0", "--tabcontent-radius-es": "0", "--tabcontent-radius-ee": "0", "--tabcontent-order": "1", width: "100%", margin: "var(--tabcontent-margin)", "border-width": "var(--border)", "border-start-start-radius": "var(--tabcontent-radius-ss)", "border-start-end-radius": "var(--tabcontent-radius-se)", "border-end-start-radius": "var(--tabcontent-radius-es)", "border-end-end-radius": "var(--tabcontent-radius-ee)" }, ".tabs-xs": { ":where(.tab)": { height: "calc(var(--size-field, 0.25rem) * 6)", "font-size": "0.75rem", "--tab-p": "0.375rem", "--tab-radius-min": "calc(0.5rem - var(--border))" } }, ".tabs-sm": { ":where(.tab)": { height: "calc(var(--size-field, 0.25rem) * 8)", "font-size": "0.875rem", "--tab-p": "0.5rem", "--tab-radius-min": "calc(0.5rem - var(--border))" } }, ".tabs-md": { ":where(.tab)": { height: "calc(var(--size-field, 0.25rem) * 10)", "font-size": "0.875rem", "--tab-p": "0.75rem", "--tab-radius-min": "calc(0.75rem - var(--border))" } }, ".tabs-lg": { ":where(.tab)": { height: "calc(var(--size-field, 0.25rem) * 12)", "font-size": "1.125rem", "--tab-p": "1rem", "--tab-radius-min": "calc(1.5rem - var(--border))" } }, ".tabs-xl": { ":where(.tab)": { height: "calc(var(--size-field, 0.25rem) * 14)", "font-size": "1.125rem", "--tab-p": "1.25rem", "--tab-radius-min": "calc(2rem - var(--border))" } } };
+
+// packages/daisyui/components/tab/index.js
+var tab_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtab = addPrefix(object_default45, prefix);
+ addComponents({ ...prefixedtab });
+};
+
+// packages/daisyui/components/filter/object.js
+var object_default46 = { ".filter": { display: "flex", "flex-wrap": "wrap", 'input[type="radio"]': { width: "auto" }, input: { overflow: "hidden", opacity: "100%", scale: "1", transition: "margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s", "&:not(:last-child)": { "margin-inline-end": "calc(0.25rem * 1)" }, "&.filter-reset": { "aspect-ratio": "1 / 1", "&::after": { content: '"×"' } } }, "&:not(:has(input:checked:not(.filter-reset)))": { '.filter-reset, input[type="reset"]': { scale: "0", "border-width": "0", "margin-inline": "calc(0.25rem * 0)", width: "calc(0.25rem * 0)", "padding-inline": "calc(0.25rem * 0)", opacity: "0%" } }, "&:has(input:checked:not(.filter-reset))": { 'input:not(:checked, .filter-reset, input[type="reset"])': { scale: "0", "border-width": "0", "margin-inline": "calc(0.25rem * 0)", width: "calc(0.25rem * 0)", "padding-inline": "calc(0.25rem * 0)", opacity: "0%" } } } };
+
+// packages/daisyui/components/filter/index.js
+var filter_default = ({ addComponents, prefix = "" }) => {
+ const prefixedfilter = addPrefix(object_default46, prefix);
+ addComponents({ ...prefixedfilter });
+};
+
+// packages/daisyui/components/chat/object.js
+var object_default47 = { ".chat": { display: "grid", "column-gap": "calc(0.25rem * 3)", "padding-block": "calc(0.25rem * 1)" }, ".chat-bubble": { position: "relative", display: "block", width: "fit-content", "border-radius": "var(--radius-field)", "background-color": "var(--color-base-300)", "padding-inline": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 2)", color: "var(--color-base-content)", "grid-row-end": "3", "min-height": "2rem", "min-width": "2.5rem", "max-width": "90%", "&:before": { position: "absolute", bottom: "calc(0.25rem * 0)", height: "calc(0.25rem * 3)", width: "calc(0.25rem * 3)", "background-color": "inherit", content: '""', "mask-repeat": "no-repeat", "mask-image": "var(--mask-chat)", "mask-position": "0px -1px", "mask-size": "13px" } }, ".chat-bubble-primary": { "background-color": "var(--color-primary)", color: "var(--color-primary-content)" }, ".chat-bubble-secondary": { "background-color": "var(--color-secondary)", color: "var(--color-secondary-content)" }, ".chat-bubble-accent": { "background-color": "var(--color-accent)", color: "var(--color-accent-content)" }, ".chat-bubble-neutral": { "background-color": "var(--color-neutral)", color: "var(--color-neutral-content)" }, ".chat-bubble-info": { "background-color": "var(--color-info)", color: "var(--color-info-content)" }, ".chat-bubble-success": { "background-color": "var(--color-success)", color: "var(--color-success-content)" }, ".chat-bubble-warning": { "background-color": "var(--color-warning)", color: "var(--color-warning-content)" }, ".chat-bubble-error": { "background-color": "var(--color-error)", color: "var(--color-error-content)" }, ".chat-image": { "grid-row": "span 2 / span 2", "align-self": "flex-end" }, ".chat-header": { "grid-row-start": "1", display: "flex", gap: "calc(0.25rem * 1)", "font-size": "0.6875rem" }, ".chat-footer": { "grid-row-start": "3", display: "flex", gap: "calc(0.25rem * 1)", "font-size": "0.6875rem" }, ".chat-start": { "place-items": "start", "grid-template-columns": "auto 1fr", ".chat-header": { "grid-column-start": "2" }, ".chat-footer": { "grid-column-start": "2" }, ".chat-image": { "grid-column-start": "1" }, ".chat-bubble": { "grid-column-start": "2", "border-end-start-radius": "0", "&:before": { transform: "rotateY(0deg)", "inset-inline-start": "-0.75rem" }, '[dir="rtl"] &:before': { transform: "rotateY(180deg)" } } }, ".chat-end": { "place-items": "end", "grid-template-columns": "1fr auto", ".chat-header": { "grid-column-start": "1" }, ".chat-footer": { "grid-column-start": "1" }, ".chat-image": { "grid-column-start": "2" }, ".chat-bubble": { "grid-column-start": "1", "border-end-end-radius": "0", "&:before": { transform: "rotateY(180deg)", "inset-inline-start": "100%" }, '[dir="rtl"] &:before': { transform: "rotateY(0deg)" } } } };
+
+// packages/daisyui/components/chat/index.js
+var chat_default = ({ addComponents, prefix = "" }) => {
+ const prefixedchat = addPrefix(object_default47, prefix);
+ addComponents({ ...prefixedchat });
+};
+
+// packages/daisyui/components/radialprogress/object.js
+var object_default48 = { ".radial-progress": { position: "relative", display: "inline-grid", height: "var(--size)", width: "var(--size)", "place-content": "center", "border-radius": "calc(infinity * 1px)", "background-color": "transparent", "vertical-align": "middle", "box-sizing": "content-box", "--value": "0", "--size": "5rem", "--thickness": "calc(var(--size) / 10)", "--radialprogress": "calc(var(--value) * 1%)", transition: "--radialprogress 0.3s linear", "&:before": { position: "absolute", inset: "calc(0.25rem * 0)", "border-radius": "calc(infinity * 1px)", content: '""', background: "radial-gradient(farthest-side, currentColor 98%, #0000) top/var(--thickness) var(--thickness) no-repeat, conic-gradient(currentColor var(--radialprogress), #0000 0)", "webkit-mask": "radial-gradient( farthest-side, #0000 calc(100% - var(--thickness)), #000 calc(100% + 0.5px - var(--thickness)) )", mask: "radial-gradient( farthest-side, #0000 calc(100% - var(--thickness)), #000 calc(100% + 0.5px - var(--thickness)) )" }, "&:after": { position: "absolute", "border-radius": "calc(infinity * 1px)", "background-color": "currentColor", transition: "transform 0.3s linear", content: '""', inset: "calc(50% - var(--thickness) / 2)", transform: "rotate(calc(var(--value) * 3.6deg - 90deg)) translate(calc(var(--size) / 2 - 50%))" } } };
+
+// packages/daisyui/components/radialprogress/index.js
+var radialprogress_default = ({ addComponents, prefix = "" }) => {
+ const prefixedradialprogress = addPrefix(object_default48, prefix);
+ addComponents({ ...prefixedradialprogress });
+};
+
+// packages/daisyui/components/countdown/object.js
+var object_default49 = { ".countdown": { display: "inline-flex", "&.countdown": { "line-height": "1em" }, "& > *": { display: "inline-block", "overflow-y": "hidden", height: "1em", "&:before": { position: "relative", content: '"00\\A 01\\A 02\\A 03\\A 04\\A 05\\A 06\\A 07\\A 08\\A 09\\A 10\\A 11\\A 12\\A 13\\A 14\\A 15\\A 16\\A 17\\A 18\\A 19\\A 20\\A 21\\A 22\\A 23\\A 24\\A 25\\A 26\\A 27\\A 28\\A 29\\A 30\\A 31\\A 32\\A 33\\A 34\\A 35\\A 36\\A 37\\A 38\\A 39\\A 40\\A 41\\A 42\\A 43\\A 44\\A 45\\A 46\\A 47\\A 48\\A 49\\A 50\\A 51\\A 52\\A 53\\A 54\\A 55\\A 56\\A 57\\A 58\\A 59\\A 60\\A 61\\A 62\\A 63\\A 64\\A 65\\A 66\\A 67\\A 68\\A 69\\A 70\\A 71\\A 72\\A 73\\A 74\\A 75\\A 76\\A 77\\A 78\\A 79\\A 80\\A 81\\A 82\\A 83\\A 84\\A 85\\A 86\\A 87\\A 88\\A 89\\A 90\\A 91\\A 92\\A 93\\A 94\\A 95\\A 96\\A 97\\A 98\\A 99\\A"', "white-space": "pre", top: "calc(var(--value) * -1em)", "text-align": "center", transition: "all 1s cubic-bezier(1, 0, 0, 1)" } } } };
+
+// packages/daisyui/components/countdown/index.js
+var countdown_default = ({ addComponents, prefix = "" }) => {
+ const prefixedcountdown = addPrefix(object_default49, prefix);
+ addComponents({ ...prefixedcountdown });
+};
+
+// packages/daisyui/components/tooltip/object.js
+var object_default50 = { ".tooltip": { position: "relative", display: "inline-block", "--tt-bg": "var(--color-neutral)", "--tt-off": "calc(100% + 0.5rem)", "--tt-tail": "calc(100% + 1px + 0.25rem)", "> :where(.tooltip-content), &:where([data-tip]):before": { position: "absolute", "max-width": "20rem", "border-radius": "var(--radius-field)", "padding-inline": "calc(0.25rem * 2)", "padding-block": "calc(0.25rem * 1)", "text-align": "center", "white-space": "normal", color: "var(--color-neutral-content)", opacity: "0%", "font-size": "0.875rem", "line-height": 1.25, transition: "opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms", "background-color": "var(--tt-bg)", width: "max-content", "pointer-events": "none", "z-index": 1, "--tw-content": "attr(data-tip)", content: "var(--tw-content)" }, "&:after": { position: ["absolute", "absolute"], opacity: "0%", "background-color": "var(--tt-bg)", transition: "opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms", content: '""', "pointer-events": "none", width: "0.625rem", height: "0.25rem", display: "block", "mask-repeat": "no-repeat", "mask-position": "-1px 0", "--mask-tooltip": `url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A")`, "mask-image": "var(--mask-tooltip)" }, '&.tooltip-open, &[data-tip]:not([data-tip=""]):hover, &:not(:has(.tooltip-content:empty)):has(.tooltip-content):hover, &:has(:focus-visible)': { "> .tooltip-content, &[data-tip]:before, &:after": { opacity: "100%", "--tt-pos": "0rem", transition: "opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0ms" } } }, ".tooltip, .tooltip-top": { "> .tooltip-content, &[data-tip]:before": { transform: "translateX(-50%) translateY(var(--tt-pos, 0.25rem))", inset: "auto auto var(--tt-off) 50%" }, "&:after": { transform: "translateX(-50%) translateY(var(--tt-pos, 0.25rem))", inset: "auto auto var(--tt-tail) 50%" } }, ".tooltip-bottom": { "> .tooltip-content, &[data-tip]:before": { transform: "translateX(-50%) translateY(var(--tt-pos, -0.25rem))", inset: "var(--tt-off) auto auto 50%" }, "&:after": { transform: "translateX(-50%) translateY(var(--tt-pos, -0.25rem)) rotate(180deg)", inset: "var(--tt-tail) auto auto 50%" } }, ".tooltip-left": { "> .tooltip-content, &[data-tip]:before": { transform: "translateX(calc(var(--tt-pos, 0.25rem) - 0.25rem)) translateY(-50%)", inset: "50% var(--tt-off) auto auto" }, "&:after": { transform: "translateX(var(--tt-pos, 0.25rem)) translateY(-50%) rotate(-90deg)", inset: "50% calc(var(--tt-tail) + 1px) auto auto" } }, ".tooltip-right": { "> .tooltip-content, &[data-tip]:before": { transform: "translateX(calc(var(--tt-pos, -0.25rem) + 0.25rem)) translateY(-50%)", inset: "50% auto auto var(--tt-off)" }, "&:after": { transform: "translateX(var(--tt-pos, -0.25rem)) translateY(-50%) rotate(90deg)", inset: "50% auto auto calc(var(--tt-tail) + 1px)" } }, ".tooltip-primary": { "--tt-bg": "var(--color-primary)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-primary-content)" } }, ".tooltip-secondary": { "--tt-bg": "var(--color-secondary)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-secondary-content)" } }, ".tooltip-accent": { "--tt-bg": "var(--color-accent)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-accent-content)" } }, ".tooltip-info": { "--tt-bg": "var(--color-info)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-info-content)" } }, ".tooltip-success": { "--tt-bg": "var(--color-success)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-success-content)" } }, ".tooltip-warning": { "--tt-bg": "var(--color-warning)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-warning-content)" } }, ".tooltip-error": { "--tt-bg": "var(--color-error)", "> .tooltip-content, &[data-tip]:before": { color: "var(--color-error-content)" } } };
+
+// packages/daisyui/components/tooltip/index.js
+var tooltip_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtooltip = addPrefix(object_default50, prefix);
+ addComponents({ ...prefixedtooltip });
+};
+
+// packages/daisyui/components/timeline/object.js
+var object_default51 = { ".timeline": { position: "relative", display: "flex", "> li": { position: "relative", display: "grid", "flex-shrink": 0, "align-items": "center", "grid-template-rows": "var(--timeline-row-start, minmax(0, 1fr)) auto var( --timeline-row-end, minmax(0, 1fr) )", "grid-template-columns": "var(--timeline-col-start, minmax(0, 1fr)) auto var( --timeline-col-end, minmax(0, 1fr) )", "> hr": { border: "none", width: "100%", "&:first-child": { "grid-column-start": "1", "grid-row-start": "2" }, "&:last-child": { "grid-column-start": "3", "grid-column-end": "none", "grid-row-start": "2", "grid-row-end": "auto" }, "@media print": { border: "0.1px solid var(--color-base-300)" } } }, ":where(hr)": { height: "calc(0.25rem * 1)", "background-color": "var(--color-base-300)" }, "&:has(.timeline-middle hr)": { "&:first-child": { "border-start-start-radius": "0", "border-end-start-radius": "0", "border-start-end-radius": "var(--radius-selector)", "border-end-end-radius": "var(--radius-selector)" }, "&:last-child": { "border-start-start-radius": "var(--radius-selector)", "border-end-start-radius": "var(--radius-selector)", "border-start-end-radius": "0", "border-end-end-radius": "0" } }, "&:not(:has(.timeline-middle))": { ":first-child hr:last-child": { "border-start-start-radius": "var(--radius-selector)", "border-end-start-radius": "var(--radius-selector)", "border-start-end-radius": "0", "border-end-end-radius": "0" }, ":last-child hr:first-child": { "border-start-start-radius": "0", "border-end-start-radius": "0", "border-start-end-radius": "var(--radius-selector)", "border-end-end-radius": "var(--radius-selector)" } } }, ".timeline-box": { border: "var(--border) solid", "border-radius": "var(--radius-box)", "border-color": "var(--color-base-300)", "background-color": "var(--color-base-100)", "padding-inline": "calc(0.25rem * 4)", "padding-block": "calc(0.25rem * 2)", "font-size": "0.75rem", "box-shadow": "0 1px 2px 0 oklch(0% 0 0/0.05)" }, ".timeline-start": { "grid-column-start": "1", "grid-column-end": "4", "grid-row-start": "1", "grid-row-end": "2", margin: "calc(0.25rem * 1)", "align-self": "flex-end", "justify-self": "center" }, ".timeline-middle": { "grid-column-start": "2", "grid-row-start": "2" }, ".timeline-end": { "grid-column-start": "1", "grid-column-end": "4", "grid-row-start": "3", "grid-row-end": "4", margin: "calc(0.25rem * 1)", "align-self": "flex-start", "justify-self": "center" }, ".timeline-compact": { "--timeline-row-start": "0", ".timeline-start": { "grid-column-start": "1", "grid-column-end": "4", "grid-row-start": "3", "grid-row-end": "4", "align-self": "flex-start", "justify-self": "center" }, "li:has(.timeline-start)": { ".timeline-end": { "grid-column-start": "none", "grid-row-start": "auto" } }, "&.timeline-vertical": { "> li": { "--timeline-col-start": "0" }, ".timeline-start": { "grid-column-start": "3", "grid-column-end": "4", "grid-row-start": "1", "grid-row-end": "4", "align-self": "center", "justify-self": "flex-start" }, "li:has(.timeline-start)": { ".timeline-end": { "grid-column-start": "auto", "grid-row-start": "none" } } } }, ".timeline-snap-icon": { "> li": { "--timeline-col-start": "0.5rem", "--timeline-row-start": "minmax(0, 1fr)" } }, ".timeline-vertical": { "flex-direction": "column", "> li": { "justify-items": "center", "--timeline-row-start": "minmax(0, 1fr)", "--timeline-row-end": "minmax(0, 1fr)", "> hr": { height: "100%", width: "calc(0.25rem * 1)", "&:first-child": { "grid-column-start": "2", "grid-row-start": "1" }, "&:last-child": { "grid-column-start": "2", "grid-column-end": "auto", "grid-row-start": "3", "grid-row-end": "none" } } }, ".timeline-start": { "grid-column-start": "1", "grid-column-end": "2", "grid-row-start": "1", "grid-row-end": "4", "align-self": "center", "justify-self": "flex-end" }, ".timeline-end": { "grid-column-start": "3", "grid-column-end": "4", "grid-row-start": "1", "grid-row-end": "4", "align-self": "center", "justify-self": "flex-start" }, "&:has(.timeline-middle)": { "> li": { "> hr": { "&:first-child": { "border-top-left-radius": "0", "border-top-right-radius": "0", "border-bottom-right-radius": "var(--radius-selector)", "border-bottom-left-radius": "var(--radius-selector)" }, "&:last-child": { "border-top-left-radius": "var(--radius-selector)", "border-top-right-radius": "var(--radius-selector)", "border-bottom-right-radius": "0", "border-bottom-left-radius": "0" } } } }, "&:not(:has(.timeline-middle))": { ":first-child": { "> hr:last-child": { "border-top-left-radius": "var(--radius-selector)", "border-top-right-radius": "var(--radius-selector)", "border-bottom-right-radius": "0", "border-bottom-left-radius": "0" } }, ":last-child": { "> hr:first-child": { "border-top-left-radius": "0", "border-top-right-radius": "0", "border-bottom-right-radius": "var(--radius-selector)", "border-bottom-left-radius": "var(--radius-selector)" } } }, "&.timeline-snap-icon": { "> li": { "--timeline-col-start": "minmax(0, 1fr)", "--timeline-row-start": "0.5rem" } } }, ".timeline-horizontal": { "flex-direction": "row", "> li": { "align-items": "center", "> hr": { height: "calc(0.25rem * 1)", width: "100%", "&:first-child": { "grid-column-start": "1", "grid-row-start": "2" }, "&:last-child": { "grid-column-start": "3", "grid-column-end": "none", "grid-row-start": "2", "grid-row-end": "auto" } } }, ".timeline-start": { "grid-column-start": "1", "grid-column-end": "4", "grid-row-start": "1", "grid-row-end": "2", "align-self": "flex-end", "justify-self": "center" }, ".timeline-end": { "grid-column-start": "1", "grid-column-end": "4", "grid-row-start": "3", "grid-row-end": "4", "align-self": "flex-start", "justify-self": "center" }, "&:has(.timeline-middle)": { "> li": { "> hr": { "&:first-child": { "border-start-start-radius": "0", "border-end-start-radius": "0", "border-start-end-radius": "var(--radius-selector)", "border-end-end-radius": "var(--radius-selector)" }, "&:last-child": { "border-start-start-radius": "var(--radius-selector)", "border-end-start-radius": "var(--radius-selector)", "border-start-end-radius": "0", "border-end-end-radius": "0" } } } }, "&:not(:has(.timeline-middle))": { ":first-child": { "> hr:last-child": { "border-start-start-radius": "var(--radius-selector)", "border-end-start-radius": "var(--radius-selector)", "border-start-end-radius": "0", "border-end-end-radius": "0" } }, ":last-child": { "> hr:first-child": { "border-start-start-radius": "0", "border-end-start-radius": "0", "border-start-end-radius": "var(--radius-selector)", "border-end-end-radius": "var(--radius-selector)" } } } } };
+
+// packages/daisyui/components/timeline/index.js
+var timeline_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtimeline = addPrefix(object_default51, prefix);
+ addComponents({ ...prefixedtimeline });
+};
+
+// packages/daisyui/components/textarea/object.js
+var object_default52 = { ".textarea": { border: "var(--border) solid #0000", "min-height": "calc(0.25rem * 20)", "flex-shrink": 1, appearance: "none", "border-radius": "var(--radius-field)", "background-color": "var(--color-base-100)", "padding-block": "calc(0.25rem * 2)", "vertical-align": "middle", width: "clamp(3rem, 20rem, 100%)", "padding-inline-start": "0.75rem", "padding-inline-end": "0.75rem", "font-size": "0.875rem", "border-color": "var(--input-color)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset", "--input-color": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", textarea: { appearance: "none", "background-color": "transparent", border: "none", "&:focus, &:focus-within": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } } }, "&:focus, &:focus-within": { "--input-color": "var(--color-base-content)", "box-shadow": "0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)", outline: "2px solid var(--input-color)", "outline-offset": "2px", isolation: "isolate" }, "&:has(> textarea[disabled]), &:is(:disabled, [disabled])": { cursor: "not-allowed", "border-color": "var(--color-base-200)", "background-color": "var(--color-base-200)", color: "color-mix(in oklab, var(--color-base-content) 40%, transparent)", "&::placeholder": { color: "color-mix(in oklab, var(--color-base-content) 20%, transparent)" }, "box-shadow": "none" }, "&:has(> textarea[disabled]) > textarea[disabled]": { cursor: "not-allowed" } }, ".textarea-ghost": { "background-color": "transparent", "box-shadow": "none", "border-color": "#0000", "&:focus, &:focus-within": { "background-color": "var(--color-base-100)", color: "var(--color-base-content)", "border-color": "#0000", "box-shadow": "none" } }, ".textarea-neutral": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-neutral)" } }, ".textarea-primary": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-primary)" } }, ".textarea-secondary": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-secondary)" } }, ".textarea-accent": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-accent)" } }, ".textarea-info": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-info)" } }, ".textarea-success": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-success)" } }, ".textarea-warning": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-warning)" } }, ".textarea-error": { "&, &:focus, &:focus-within": { "--input-color": "var(--color-error)" } }, ".textarea-xs": { "font-size": "0.6875rem" }, ".textarea-sm": { "font-size": "0.75rem" }, ".textarea-md": { "font-size": "0.875rem" }, ".textarea-lg": { "font-size": "1.125rem" }, ".textarea-xl": { "font-size": "1.375rem" } };
+
+// packages/daisyui/components/textarea/index.js
+var textarea_default = ({ addComponents, prefix = "" }) => {
+ const prefixedtextarea = addPrefix(object_default52, prefix);
+ addComponents({ ...prefixedtextarea });
+};
+
+// packages/daisyui/components/range/object.js
+var object_default53 = { ".range": { appearance: "none", "webkit-appearance": "none", "--range-thumb": "var(--color-base-100)", "--range-thumb-size": "calc(var(--size-selector, 0.25rem) * 6)", "--range-progress": "currentColor", "--range-fill": "1", "--range-p": "0.25rem", "--range-bg": "color-mix(in oklab, currentColor 10%, #0000)", cursor: "pointer", overflow: "hidden", "background-color": "transparent", "vertical-align": "middle", width: "clamp(3rem, 20rem, 100%)", "--radius-selector-max": `calc(
+ var(--radius-selector) + var(--radius-selector) + var(--radius-selector)
+ )`, "border-radius": "calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)))", border: "none", height: "var(--range-thumb-size)", '[dir="rtl"] &': { "--range-dir": "-1" }, "&:focus": { outline: "none" }, "&:focus-visible": { outline: "2px solid", "outline-offset": "2px" }, "&::-webkit-slider-runnable-track": { width: "100%", "background-color": "var(--range-bg)", "border-radius": "var(--radius-selector)", height: "calc(var(--range-thumb-size) * 0.5)" }, "@media (forced-colors: active)": [{ "&::-webkit-slider-runnable-track": { border: "1px solid" } }, { "&::-moz-range-track": { border: "1px solid" } }], "&::-webkit-slider-thumb": { position: "relative", "box-sizing": "border-box", "border-radius": "calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)))", "background-color": "currentColor", height: "var(--range-thumb-size)", width: "var(--range-thumb-size)", border: "var(--range-p) solid", appearance: "none", "webkit-appearance": "none", top: "50%", color: "var(--range-progress)", transform: "translateY(-50%)", "box-shadow": "0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill))" }, "&::-moz-range-track": { width: "100%", "background-color": "var(--range-bg)", "border-radius": "var(--radius-selector)", height: "calc(var(--range-thumb-size) * 0.5)" }, "&::-moz-range-thumb": { position: "relative", "box-sizing": "border-box", "border-radius": "calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)))", "background-color": "currentColor", height: "var(--range-thumb-size)", width: "var(--range-thumb-size)", border: "var(--range-p) solid", top: "50%", color: "var(--range-progress)", "box-shadow": "0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill))" }, "&:disabled": { cursor: "not-allowed", opacity: "30%" } }, ".range-primary": { color: "var(--color-primary)", "--range-thumb": "var(--color-primary-content)" }, ".range-secondary": { color: "var(--color-secondary)", "--range-thumb": "var(--color-secondary-content)" }, ".range-accent": { color: "var(--color-accent)", "--range-thumb": "var(--color-accent-content)" }, ".range-neutral": { color: "var(--color-neutral)", "--range-thumb": "var(--color-neutral-content)" }, ".range-success": { color: "var(--color-success)", "--range-thumb": "var(--color-success-content)" }, ".range-warning": { color: "var(--color-warning)", "--range-thumb": "var(--color-warning-content)" }, ".range-info": { color: "var(--color-info)", "--range-thumb": "var(--color-info-content)" }, ".range-error": { color: "var(--color-error)", "--range-thumb": "var(--color-error-content)" }, ".range-xs": { "--range-thumb-size": "calc(var(--size-selector, 0.25rem) * 4)" }, ".range-sm": { "--range-thumb-size": "calc(var(--size-selector, 0.25rem) * 5)" }, ".range-md": { "--range-thumb-size": "calc(var(--size-selector, 0.25rem) * 6)" }, ".range-lg": { "--range-thumb-size": "calc(var(--size-selector, 0.25rem) * 7)" }, ".range-xl": { "--range-thumb-size": "calc(var(--size-selector, 0.25rem) * 8)" } };
+
+// packages/daisyui/components/range/index.js
+var range_default = ({ addComponents, prefix = "" }) => {
+ const prefixedrange = addPrefix(object_default53, prefix);
+ addComponents({ ...prefixedrange });
+};
+
+// packages/daisyui/components/dock/object.js
+var object_default54 = { ".dock": { position: "fixed", right: "calc(0.25rem * 0)", bottom: "calc(0.25rem * 0)", left: "calc(0.25rem * 0)", "z-index": 1, display: "flex", width: "100%", "flex-direction": "row", "align-items": "center", "justify-content": "space-around", "background-color": "var(--color-base-100)", padding: "calc(0.25rem * 2)", color: "currentColor", "border-top": "0.5px solid color-mix(in oklab, var(--color-base-content) 5%, #0000)", height: ["4rem", "calc(4rem + env(safe-area-inset-bottom))"], "padding-bottom": "env(safe-area-inset-bottom)", "> *": { position: "relative", "margin-bottom": "calc(0.25rem * 2)", display: "flex", height: "100%", "max-width": "calc(0.25rem * 32)", "flex-shrink": 1, "flex-basis": "100%", cursor: "pointer", "flex-direction": "column", "align-items": "center", "justify-content": "center", gap: "1px", "border-radius": "var(--radius-box)", "background-color": "transparent", transition: "opacity 0.2s ease-out", "@media (hover: hover)": { "&:hover": { opacity: "80%" } }, '&[aria-disabled="true"], &[disabled]': { "&, &:hover": { "pointer-events": "none", color: "color-mix(in oklab, var(--color-base-content) 10%, transparent)", opacity: "100%" } }, ".dock-label": { "font-size": "0.6875rem" }, "&:after": { content: '""', position: "absolute", height: "calc(0.25rem * 1)", width: "calc(0.25rem * 6)", "border-radius": "calc(infinity * 1px)", "background-color": "transparent", bottom: "0.2rem", "border-top": "3px solid transparent", transition: "background-color 0.1s ease-out, text-color 0.1s ease-out, width 0.1s ease-out" } } }, ".dock-active": { "&:after": { width: "calc(0.25rem * 10)", "background-color": "currentColor", color: "currentColor" } }, ".dock-xs": { height: ["3rem", "calc(3rem + env(safe-area-inset-bottom))"], ".dock-active": { "&:after": { bottom: "-0.1rem" } }, ".dock-label": { "font-size": "0.625rem" } }, ".dock-sm": { height: ["calc(0.25rem * 14)", "3.5rem", "calc(3.5rem + env(safe-area-inset-bottom))"], ".dock-active": { "&:after": { bottom: "-0.1rem" } }, ".dock-label": { "font-size": "0.625rem" } }, ".dock-md": { height: ["4rem", "calc(4rem + env(safe-area-inset-bottom))"], ".dock-label": { "font-size": "0.6875rem" } }, ".dock-lg": { height: ["4.5rem", "calc(4.5rem + env(safe-area-inset-bottom))"], ".dock-active": { "&:after": { bottom: "0.4rem" } }, ".dock-label": { "font-size": "0.6875rem" } }, ".dock-xl": { height: ["5rem", "calc(5rem + env(safe-area-inset-bottom))"], ".dock-active": { "&:after": { bottom: "0.4rem" } }, ".dock-label": { "font-size": "0.75rem" } } };
+
+// packages/daisyui/components/dock/index.js
+var dock_default = ({ addComponents, prefix = "" }) => {
+ const prefixeddock = addPrefix(object_default54, prefix);
+ addComponents({ ...prefixeddock });
+};
+
+// packages/daisyui/components/breadcrumbs/object.js
+var object_default55 = { ".breadcrumbs": { "max-width": "100%", "overflow-x": "auto", "padding-block": "calc(0.25rem * 2)", "> menu, > ul, > ol": { display: "flex", "min-height": "min-content", "align-items": "center", "white-space": "nowrap", "> li": { display: "flex", "align-items": "center", "> *": { display: "flex", cursor: "pointer", "align-items": "center", gap: "calc(0.25rem * 2)", "&:hover": { "@media (hover: hover)": { "text-decoration-line": "underline" } }, "&:focus": { "--tw-outline-style": "none", "outline-style": "none", "@media (forced-colors: active)": { outline: "2px solid transparent", "outline-offset": "2px" } }, "&:focus-visible": { outline: "2px solid currentColor", "outline-offset": "2px" } }, "& + *:before": { content: '""', "margin-right": "calc(0.25rem * 3)", "margin-left": "calc(0.25rem * 2)", display: "block", height: "calc(0.25rem * 1.5)", width: "calc(0.25rem * 1.5)", opacity: "40%", rotate: "45deg", "border-top": "1px solid", "border-right": "1px solid", "background-color": "#0000" }, '[dir="rtl"] & + *:before': { rotate: "-135deg" } } } } };
+
+// packages/daisyui/components/breadcrumbs/index.js
+var breadcrumbs_default = ({ addComponents, prefix = "" }) => {
+ const prefixedbreadcrumbs = addPrefix(object_default55, prefix);
+ addComponents({ ...prefixedbreadcrumbs });
+};
+
+// packages/daisyui/components/radio/object.js
+var object_default56 = { ".radio": { position: "relative", "flex-shrink": 0, cursor: "pointer", appearance: "none", "border-radius": "calc(infinity * 1px)", padding: "calc(0.25rem * 1)", "vertical-align": "middle", border: "var(--border) solid var(--input-color, color-mix(in srgb, currentColor 20%, #0000))", "box-shadow": "0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset", "--size": "calc(var(--size-selector, 0.25rem) * 6)", width: "var(--size)", height: "var(--size)", color: "var(--input-color, currentColor)", "&:before": { display: "block", width: "100%", height: "100%", "border-radius": "calc(infinity * 1px)", "--tw-content": '""', content: "var(--tw-content)", "background-size": "auto, calc(var(--noise) * 100%)", "background-image": "none, var(--fx-noise)" }, "&:focus-visible": { outline: "2px solid currentColor" }, '&:checked, &[aria-checked="true"]': { animation: "radio 0.2s ease-out", "border-color": "currentColor", "background-color": "var(--color-base-100)", "&:before": { "background-color": "currentColor", "box-shadow": "0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1))" }, "@media (forced-colors: active)": { "&:before": { "outline-style": "var(--tw-outline-style)", "outline-width": "1px", "outline-offset": "calc(1px * -1)" } }, "@media print": { "&:before": { outline: "0.25rem solid", "outline-offset": "-1rem" } } } }, ".radio-primary": { "--input-color": "var(--color-primary)" }, ".radio-secondary": { "--input-color": "var(--color-secondary)" }, ".radio-accent": { "--input-color": "var(--color-accent)" }, ".radio-neutral": { "--input-color": "var(--color-neutral)" }, ".radio-info": { "--input-color": "var(--color-info)" }, ".radio-success": { "--input-color": "var(--color-success)" }, ".radio-warning": { "--input-color": "var(--color-warning)" }, ".radio-error": { "--input-color": "var(--color-error)" }, ".radio:disabled": { cursor: "not-allowed", opacity: "20%" }, ".radio-xs": { padding: "0.125rem", '&:is([type="radio"])': { "--size": "calc(var(--size-selector, 0.25rem) * 4)" } }, ".radio-sm": { padding: "0.1875rem", '&:is([type="radio"])': { "--size": "calc(var(--size-selector, 0.25rem) * 5)" } }, ".radio-md": { padding: "0.25rem", '&:is([type="radio"])': { "--size": "calc(var(--size-selector, 0.25rem) * 6)" } }, ".radio-lg": { padding: "0.3125rem", '&:is([type="radio"])': { "--size": "calc(var(--size-selector, 0.25rem) * 7)" } }, ".radio-xl": { padding: "0.375rem", '&:is([type="radio"])': { "--size": "calc(var(--size-selector, 0.25rem) * 8)" } }, "@keyframes radio": { "0%": { padding: "5px" }, "50%": { padding: "3px" } } };
+
+// packages/daisyui/components/radio/index.js
+var radio_default = ({ addComponents, prefix = "" }) => {
+ const prefixedradio = addPrefix(object_default56, prefix);
+ addComponents({ ...prefixedradio });
+};
+
+// packages/daisyui/components/skeleton/object.js
+var object_default57 = { ".skeleton": { "border-radius": "var(--radius-box)", "background-color": "var(--color-base-300)", "@media (prefers-reduced-motion: reduce)": { "transition-duration": "15s" }, "will-change": "background-position", animation: "skeleton 1.8s ease-in-out infinite", "background-image": "linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% )", "background-size": "200% auto", "background-repeat": "no-repeat", "background-position-x": "-50%" }, "@keyframes skeleton": { "0%": { "background-position": "150%" }, "100%": { "background-position": "-50%" } } };
+
+// packages/daisyui/components/skeleton/index.js
+var skeleton_default = ({ addComponents, prefix = "" }) => {
+ const prefixedskeleton = addPrefix(object_default57, prefix);
+ addComponents({ ...prefixedskeleton });
+};
+
+// packages/daisyui/components/loading/object.js
+var object_default58 = { ".loading": { "pointer-events": "none", display: "inline-block", "aspect-ratio": "1 / 1", "background-color": "currentColor", "vertical-align": "middle", width: "calc(var(--size-selector, 0.25rem) * 6)", "mask-size": "100%", "mask-repeat": "no-repeat", "mask-position": "center", "mask-image": `url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")` }, ".loading-spinner": { "mask-image": `url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")` }, ".loading-dots": { "mask-image": `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='4' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1'/%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1' begin='0.1s'/%3E%3C/circle%3E%3Ccircle cx='20' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1' begin='0.2s'/%3E%3C/circle%3E%3C/svg%3E")` }, ".loading-ring": { "mask-image": `url("data:image/svg+xml,%3Csvg width='44' height='44' viewBox='0 0 44 44' xmlns='http://www.w3.org/2000/svg' stroke='white'%3E%3Cg fill='none' fill-rule='evenodd' stroke-width='2'%3E%3Ccircle cx='22' cy='22' r='1'%3E%3Canimate attributeName='r' begin='0s' dur='1.8s' values='1;20' calcMode='spline' keyTimes='0;1' keySplines='0.165,0.84,0.44,1' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-opacity' begin='0s' dur='1.8s' values='1;0' calcMode='spline' keyTimes='0;1' keySplines='0.3,0.61,0.355,1' repeatCount='indefinite'/%3E%3C/circle%3E%3Ccircle cx='22' cy='22' r='1'%3E%3Canimate attributeName='r' begin='-0.9s' dur='1.8s' values='1;20' calcMode='spline' keyTimes='0;1' keySplines='0.165,0.84,0.44,1' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-opacity' begin='-0.9s' dur='1.8s' values='1;0' calcMode='spline' keyTimes='0;1' keySplines='0.3,0.61,0.355,1' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")` }, ".loading-ball": { "mask-image": `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cellipse cx='12' cy='5' rx='4' ry='4'%3E%3Canimate attributeName='cy' values='5;20;20.5;20;5' keyTimes='0;0.469;0.5;0.531;1' dur='.8s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1'/%3E%3Canimate attributeName='rx' values='4;4;4.8;4;4' keyTimes='0;0.469;0.5;0.531;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='ry' values='4;4;3;4;4' keyTimes='0;0.469;0.5;0.531;1' dur='.8s' repeatCount='indefinite'/%3E%3C/ellipse%3E%3C/svg%3E")` }, ".loading-bars": { "mask-image": `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3C/rect%3E%3Crect x='9' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3C/rect%3E%3Crect x='17' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3C/rect%3E%3C/svg%3E")` }, ".loading-infinity": { "mask-image": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='shape-rendering:auto;' width='200px' height='200px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Cpath fill='none' stroke='black' stroke-width='10' stroke-dasharray='205.271 51.318' d='M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z' stroke-linecap='round' style='transform:scale(0.8);transform-origin:50px 50px'%3E%3Canimate attributeName='stroke-dashoffset' repeatCount='indefinite' dur='2s' keyTimes='0;1' values='0;256.589'/%3E%3C/path%3E%3C/svg%3E")` }, ".loading-xs": { width: "calc(var(--size-selector, 0.25rem) * 4)" }, ".loading-sm": { width: "calc(var(--size-selector, 0.25rem) * 5)" }, ".loading-md": { width: "calc(var(--size-selector, 0.25rem) * 6)" }, ".loading-lg": { width: "calc(var(--size-selector, 0.25rem) * 7)" }, ".loading-xl": { width: "calc(var(--size-selector, 0.25rem) * 8)" } };
+
+// packages/daisyui/components/loading/index.js
+var loading_default = ({ addComponents, prefix = "" }) => {
+ const prefixedloading = addPrefix(object_default58, prefix);
+ addComponents({ ...prefixedloading });
+};
+
+// packages/daisyui/components/validator/object.js
+var object_default59 = { ".validator": { "&:user-valid, &:has(:user-valid)": { '&, &:focus, &:checked, &[aria-checked="true"], &:focus-within': { "--input-color": "var(--color-success)" } }, "&:user-invalid, &:has(:user-invalid), &[aria-invalid]": { '&, &:focus, &:checked, &[aria-checked="true"], &:focus-within': { "--input-color": "var(--color-error)" }, "& ~ .validator-hint": { visibility: "visible", display: "block", color: "var(--color-error)" } } }, ".validator-hint": { visibility: "hidden", "margin-top": "calc(0.25rem * 2)", "font-size": "0.75rem" } };
+
+// packages/daisyui/components/validator/index.js
+var validator_default = ({ addComponents, prefix = "" }) => {
+ const prefixedvalidator = addPrefix(object_default59, prefix);
+ addComponents({ ...prefixedvalidator });
+};
+
+// packages/daisyui/components/collapse/object.js
+var object_default60 = { ".collapse:not(td, tr, colgroup)": { visibility: "visible" }, ".collapse": { position: "relative", display: "grid", overflow: "hidden", "border-radius": "var(--radius-box, 1rem)", width: "100%", "grid-template-rows": "max-content 0fr", transition: "grid-template-rows 0.2s", isolation: "isolate", '> input:is([type="checkbox"], [type="radio"])': { "grid-column-start": "1", "grid-row-start": "1", appearance: "none", opacity: 0, "z-index": 1, width: "100%", padding: "1rem", "padding-inline-end": "3rem", "min-height": "3.75rem", transition: "background-color 0.2s ease-out" }, '&:is([open], :focus:not(.collapse-close)), &:not(.collapse-close):has(> input:is([type="checkbox"], [type="radio"]):checked)': { "grid-template-rows": "max-content 1fr" }, '&:is([open], :focus:not(.collapse-close)) > .collapse-content, &:not(.collapse-close) > :where(input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-content)': { visibility: "visible", "min-height": "fit-content" }, '&:focus-visible, &:has(> input:is([type="checkbox"], [type="radio"]):focus-visible)': { "outline-color": "var(--color-base-content)", "outline-style": "solid", "outline-width": "2px", "outline-offset": "2px" }, "&:not(.collapse-close)": { '> input[type="checkbox"], > input[type="radio"]:not(:checked), > .collapse-title': { cursor: "pointer" } }, "&:focus:not(.collapse-close, .collapse[open]) > .collapse-title": { cursor: "unset" }, '&:is([open], :focus:not(.collapse-close)) > :where(.collapse-content), &:not(.collapse-close) > :where(input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-content)': { "padding-bottom": "1rem", transition: "padding 0.2s ease-out, background-color 0.2s ease-out" }, "&:is([open])": { "&.collapse-arrow": { "> .collapse-title:after": { transform: "translateY(-50%) rotate(225deg)" } } }, "&.collapse-open": { "&.collapse-arrow": { "> .collapse-title:after": { transform: "translateY(-50%) rotate(225deg)" } }, "&.collapse-plus": { "> .collapse-title:after": { content: '"−"' } } }, "&.collapse-arrow:focus:not(.collapse-close)": { "> .collapse-title:after": { transform: "translateY(-50%) rotate(225deg)" } }, "&.collapse-arrow:not(.collapse-close)": { '> input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-title:after': { transform: "translateY(-50%) rotate(225deg)" } }, "&[open]": { "&.collapse-plus": { "> .collapse-title:after": { content: '"−"' } } }, "&.collapse-plus:focus:not(.collapse-close)": { "> .collapse-title:after": { content: '"−"' } }, "&.collapse-plus:not(.collapse-close)": { '> input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-title:after': { content: '"−"' } } }, ".collapse-title, .collapse-content": { "grid-column-start": "1", "grid-row-start": "1" }, ".collapse-content": { visibility: "hidden", "grid-column-start": "1", "grid-row-start": "2", "min-height": "0", "padding-left": "1rem", "padding-right": "1rem", cursor: "unset", transition: "visibility 0.2s, padding 0.2s ease-out, background-color 0.2s ease-out" }, ".collapse:is(details)": { width: "100%", "& summary": { position: "relative", display: "block", "&::-webkit-details-marker": { display: "none" } } }, ".collapse:is(details) summary": { outline: "none" }, ".collapse-arrow": { "> .collapse-title:after": { position: "absolute", display: "block", height: "0.5rem", width: "0.5rem", transform: "translateY(-100%) rotate(45deg)", "transition-property": "all", "transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)", "transition-duration": "0.2s", top: "1.9rem", "inset-inline-end": "1.4rem", content: '""', "transform-origin": "75% 75%", "box-shadow": "2px 2px", "pointer-events": "none" } }, ".collapse-plus": { "> .collapse-title:after": { position: "absolute", display: "block", height: "0.5rem", width: "0.5rem", "transition-property": "all", "transition-duration": "300ms", "transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)", top: "0.9rem", "inset-inline-end": "1.4rem", content: '"+"', "pointer-events": "none" } }, ".collapse-title": { position: "relative", width: "100%", padding: "1rem", "padding-inline-end": "3rem", "min-height": "3.75rem", transition: "background-color 0.2s ease-out" }, ".collapse-open": { "grid-template-rows": "max-content 1fr", "> .collapse-content": { visibility: "visible", "min-height": "fit-content", "padding-bottom": "1rem", transition: "padding 0.2s ease-out, background-color 0.2s ease-out" } } };
+
+// packages/daisyui/components/collapse/index.js
+var collapse_default = ({ addComponents, prefix = "" }) => {
+ const prefixedcollapse = addPrefix(object_default60, prefix);
+ addComponents({ ...prefixedcollapse });
+};
+
+// packages/daisyui/components/swap/object.js
+var object_default61 = { ".swap": { position: "relative", display: "inline-grid", cursor: "pointer", "place-content": "center", "vertical-align": "middle", "webkit-user-select": "none", "user-select": "none", input: { appearance: "none", border: "none" }, "> *": { "grid-column-start": "1", "grid-row-start": "1", "transition-property": "transform, rotate, opacity", "transition-duration": "0.2s", "transition-timing-function": "cubic-bezier(0, 0, 0.2, 1)" }, ".swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on": { opacity: "0%" }, "input:is(:checked, :indeterminate)": { "& ~ .swap-off": { opacity: "0%" } }, "input:checked ~ .swap-on, input:indeterminate ~ .swap-indeterminate": { opacity: "100%", "backface-visibility": "visible" } }, ".swap-active": { ".swap-off": { opacity: "0%" }, ".swap-on": { opacity: "100%" } }, ".swap-rotate": { ".swap-on, input:indeterminate ~ .swap-on": { rotate: "45deg" }, "input:is(:checked, :indeterminate) ~ .swap-on, &.swap-active .swap-on": { rotate: "0deg" }, "input:is(:checked, :indeterminate) ~ .swap-off, &.swap-active .swap-off": { rotate: "calc(45deg * -1)" } }, ".swap-flip": { "transform-style": "preserve-3d", perspective: "20rem", ".swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on": { transform: "rotateY(180deg)", "backface-visibility": "hidden" }, "input:is(:checked, :indeterminate) ~ .swap-on, &.swap-active .swap-on": { transform: "rotateY(0deg)" }, "input:is(:checked, :indeterminate) ~ .swap-off, &.swap-active .swap-off": { transform: "rotateY(-180deg)", "backface-visibility": "hidden", opacity: "100%" } } };
+
+// packages/daisyui/components/swap/index.js
+var swap_default = ({ addComponents, prefix = "" }) => {
+ const prefixedswap = addPrefix(object_default61, prefix);
+ addComponents({ ...prefixedswap });
+};
+
+// packages/daisyui/utilities/typography/object.js
+var object_default62 = { ":root .prose": { "--tw-prose-body": "color-mix(in oklab, var(--color-base-content) 80%, #0000)", "--tw-prose-headings": "var(--color-base-content)", "--tw-prose-lead": "var(--color-base-content)", "--tw-prose-links": "var(--color-base-content)", "--tw-prose-bold": "var(--color-base-content)", "--tw-prose-counters": "var(--color-base-content)", "--tw-prose-bullets": "color-mix(in oklab, var(--color-base-content) 50%, #0000)", "--tw-prose-hr": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", "--tw-prose-quotes": "var(--color-base-content)", "--tw-prose-quote-borders": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", "--tw-prose-captions": "color-mix(in oklab, var(--color-base-content) 50%, #0000)", "--tw-prose-code": "var(--color-base-content)", "--tw-prose-pre-code": "var(--color-neutral-content)", "--tw-prose-pre-bg": "var(--color-neutral)", "--tw-prose-th-borders": "color-mix(in oklab, var(--color-base-content) 50%, #0000)", "--tw-prose-td-borders": "color-mix(in oklab, var(--color-base-content) 20%, #0000)", "--tw-prose-kbd": "color-mix(in oklab, var(--color-base-content) 80%, #0000)", ":where(code):not(pre > code)": { "background-color": "var(--color-base-200)", "border-radius": "var(--radius-selector)", border: "var(--border) solid var(--color-base-300)", "padding-inline": "0.5em", "font-weight": "inherit", "&:before, &:after": { display: "none" } } } };
+
+// packages/daisyui/utilities/typography/index.js
+var typography_default = ({ addUtilities, prefix = "" }) => {
+ const prefixedtypography = addPrefix(object_default62, prefix);
+ addUtilities({ ...prefixedtypography });
+};
+
+// packages/daisyui/utilities/glass/object.js
+var object_default63 = { ".glass": { border: "none", "backdrop-filter": "blur(var(--glass-blur, 40px))", "background-color": "#0000", "background-image": "linear-gradient( 135deg, oklch(100% 0 0 / var(--glass-opacity, 30%)) 0%, oklch(0% 0 0 / 0%) 100% ), linear-gradient( var(--glass-reflect-degree, 100deg), oklch(100% 0 0 / var(--glass-reflect-opacity, 5%)) 25%, oklch(0% 0 0 / 0%) 25% )", "box-shadow": "0 0 0 1px oklch(100% 0 0 / var(--glass-border-opacity, 20%)) inset, 0 0 0 2px oklch(0% 0 0 / 5%)", "text-shadow": "0 1px oklch(0% 0 0 / var(--glass-text-shadow-opacity, 5%))" } };
+
+// packages/daisyui/utilities/glass/index.js
+var glass_default = ({ addUtilities, prefix = "" }) => {
+ const prefixedglass = addPrefix(object_default63, prefix);
+ addUtilities({ ...prefixedglass });
+};
+
+// packages/daisyui/utilities/join/object.js
+var object_default64 = { ".join": { display: "inline-flex", "align-items": "stretch", "--join-ss": "0", "--join-se": "0", "--join-es": "0", "--join-ee": "0", ":where(.join-item)": { "border-start-start-radius": "var(--join-ss, 0)", "border-start-end-radius": "var(--join-se, 0)", "border-end-start-radius": "var(--join-es, 0)", "border-end-end-radius": "var(--join-ee, 0)", "*": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" } }, "> .join-item:where(:first-child)": { "--join-ss": "var(--radius-field)", "--join-se": "0", "--join-es": "var(--radius-field)", "--join-ee": "0" }, ":first-child:not(:last-child)": { ":where(.join-item)": { "--join-ss": "var(--radius-field)", "--join-se": "0", "--join-es": "var(--radius-field)", "--join-ee": "0" } }, "> .join-item:where(:last-child)": { "--join-ss": "0", "--join-se": "var(--radius-field)", "--join-es": "0", "--join-ee": "var(--radius-field)" }, ":last-child:not(:first-child)": { ":where(.join-item)": { "--join-ss": "0", "--join-se": "var(--radius-field)", "--join-es": "0", "--join-ee": "var(--radius-field)" } }, "> .join-item:where(:only-child)": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" }, ":only-child": { ":where(.join-item)": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" } } }, ".join-item": { "&:where(*:not(:first-child, :disabled, [disabled], .btn-disabled))": { "margin-inline-start": "calc(var(--border, 1px) * -1)", "margin-block-start": "0" } }, ".join-vertical": { "flex-direction": "column", "> .join-item:first-child": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "0", "--join-ee": "0" }, ":first-child:not(:last-child)": { ".join-item": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "0", "--join-ee": "0" } }, "> .join-item:last-child": { "--join-ss": "0", "--join-se": "0", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" }, ":last-child:not(:first-child)": { ".join-item": { "--join-ss": "0", "--join-se": "0", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" } }, "> .join-item:only-child": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" }, ":only-child": { ".join-item": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" } }, ".join-item": { "&:where(*:not(:first-child))": { "margin-inline-start": "0", "margin-block-start": "calc(var(--border, 1px) * -1)" } } }, ".join-horizontal": { "flex-direction": "row", "> .join-item:first-child": { "--join-ss": "var(--radius-field)", "--join-se": "0", "--join-es": "var(--radius-field)", "--join-ee": "0" }, ":first-child:not(:last-child)": { ".join-item": { "--join-ss": "var(--radius-field)", "--join-se": "0", "--join-es": "var(--radius-field)", "--join-ee": "0" } }, "> .join-item:last-child": { "--join-ss": "0", "--join-se": "var(--radius-field)", "--join-es": "0", "--join-ee": "var(--radius-field)" }, ":last-child:not(:first-child)": { ".join-item": { "--join-ss": "0", "--join-se": "var(--radius-field)", "--join-es": "0", "--join-ee": "var(--radius-field)" } }, "> .join-item:only-child": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" }, ":only-child": { ".join-item": { "--join-ss": "var(--radius-field)", "--join-se": "var(--radius-field)", "--join-es": "var(--radius-field)", "--join-ee": "var(--radius-field)" } }, ".join-item": { "&:where(*:not(:first-child))": { "margin-inline-start": "calc(var(--border, 1px) * -1)", "margin-block-start": "0" } } } };
+
+// packages/daisyui/utilities/join/index.js
+var join_default = ({ addUtilities, prefix = "" }) => {
+ const prefixedjoin = addPrefix(object_default64, prefix);
+ addUtilities({ ...prefixedjoin });
+};
+
+// packages/daisyui/utilities/radius/object.js
+var object_default65 = { ".rounded-box": { "border-radius": "var(--radius-box)" }, ".rounded-field": { "border-radius": "var(--radius-field)" }, ".rounded-selector": { "border-radius": "var(--radius-selector)" }, ".rounded-t-box": { "border-top-left-radius": "var(--radius-box)", "border-top-right-radius": "var(--radius-box)" }, ".rounded-b-box": { "border-bottom-left-radius": "var(--radius-box)", "border-bottom-right-radius": "var(--radius-box)" }, ".rounded-l-box": { "border-top-left-radius": "var(--radius-box)", "border-bottom-left-radius": "var(--radius-box)" }, ".rounded-r-box": { "border-top-right-radius": "var(--radius-box)", "border-bottom-right-radius": "var(--radius-box)" }, ".rounded-tl-box": { "border-top-left-radius": "var(--radius-box)" }, ".rounded-tr-box": { "border-top-right-radius": "var(--radius-box)" }, ".rounded-br-box": { "border-bottom-right-radius": "var(--radius-box)" }, ".rounded-bl-box": { "border-bottom-left-radius": "var(--radius-box)" }, ".rounded-t-field": { "border-top-left-radius": "var(--radius-field)", "border-top-right-radius": "var(--radius-field)" }, ".rounded-b-field": { "border-bottom-left-radius": "var(--radius-field)", "border-bottom-right-radius": "var(--radius-field)" }, ".rounded-l-field": { "border-top-left-radius": "var(--radius-field)", "border-bottom-left-radius": "var(--radius-field)" }, ".rounded-r-field": { "border-top-right-radius": "var(--radius-field)", "border-bottom-right-radius": "var(--radius-field)" }, ".rounded-tl-field": { "border-top-left-radius": "var(--radius-field)" }, ".rounded-tr-field": { "border-top-right-radius": "var(--radius-field)" }, ".rounded-br-field": { "border-bottom-right-radius": "var(--radius-field)" }, ".rounded-bl-field": { "border-bottom-left-radius": "var(--radius-field)" }, ".rounded-t-selector": { "border-top-left-radius": "var(--radius-selector)", "border-top-right-radius": "var(--radius-selector)" }, ".rounded-b-selector": { "border-bottom-left-radius": "var(--radius-selector)", "border-bottom-right-radius": "var(--radius-selector)" }, ".rounded-l-selector": { "border-top-left-radius": "var(--radius-selector)", "border-bottom-left-radius": "var(--radius-selector)" }, ".rounded-r-selector": { "border-top-right-radius": "var(--radius-selector)", "border-bottom-right-radius": "var(--radius-selector)" }, ".rounded-tl-selector": { "border-top-left-radius": "var(--radius-selector)" }, ".rounded-tr-selector": { "border-top-right-radius": "var(--radius-selector)" }, ".rounded-br-selector": { "border-bottom-right-radius": "var(--radius-selector)" }, ".rounded-bl-selector": { "border-bottom-left-radius": "var(--radius-selector)" } };
+
+// packages/daisyui/utilities/radius/index.js
+var radius_default = ({ addUtilities, prefix = "" }) => {
+ const prefixedradius = addPrefix(object_default65, prefix);
+ addUtilities({ ...prefixedradius });
+};
+
+// packages/daisyui/imports.js
+var base = { rootscrolllock: rootscrolllock_default, rootcolor: rootcolor_default, scrollbar: scrollbar_default, properties: properties_default, rootscrollgutter: rootscrollgutter_default, svg: svg_default };
+var components = { drawer: drawer_default, link: link_default, stat: stat_default, carousel: carousel_default, divider: divider_default, mask: mask_default, fieldset: fieldset_default, dropdown: dropdown_default, card: card_default, steps: steps_default, alert: alert_default, kbd: kbd_default, select: select_default, progress: progress_default, fileinput: fileinput_default, modal: modal_default, footer: footer_default, table: table_default, avatar: avatar_default, input: input_default, checkbox: checkbox_default, badge: badge_default, status: status_default, diff: diff_default, hero: hero_default, toggle: toggle_default, stack: stack_default, navbar: navbar_default, label: label_default, menu: menu_default, toast: toast_default, button: button_default, list: list_default, mockup: mockup_default, calendar: calendar_default, indicator: indicator_default, rating: rating_default, tab: tab_default, filter: filter_default, chat: chat_default, radialprogress: radialprogress_default, countdown: countdown_default, tooltip: tooltip_default, timeline: timeline_default, textarea: textarea_default, range: range_default, dock: dock_default, breadcrumbs: breadcrumbs_default, radio: radio_default, skeleton: skeleton_default, loading: loading_default, validator: validator_default, collapse: collapse_default, swap: swap_default };
+var utilities = { typography: typography_default, glass: glass_default, join: join_default, radius: radius_default };
+
+// packages/daisyui/index.js
+var version = "5.0.35";
+var daisyui_default = plugin.withOptions((options) => {
+ return ({ addBase, addComponents, addUtilities }) => {
+ const {
+ include,
+ exclude,
+ prefix = ""
+ } = pluginOptionsHandler(options, addBase, object_default, version);
+ const shouldIncludeItem = (name) => {
+ if (include && exclude) {
+ return include.includes(name) && !exclude.includes(name);
+ }
+ if (include) {
+ return include.includes(name);
+ }
+ if (exclude) {
+ return !exclude.includes(name);
+ }
+ return true;
+ };
+ Object.entries(base).forEach(([name, item]) => {
+ if (!shouldIncludeItem(name))
+ return;
+ item({ addBase, prefix });
+ });
+ Object.entries(components).forEach(([name, item]) => {
+ if (!shouldIncludeItem(name))
+ return;
+ item({ addComponents, prefix });
+ });
+ Object.entries(utilities).forEach(([name, item]) => {
+ if (!shouldIncludeItem(name))
+ return;
+ item({ addUtilities, prefix });
+ });
+ };
+}, () => ({
+ theme: {
+ extend: variables_default
+ }
+}));
+
+
+/*
+
+ MIT License
+
+ Copyright (c) 2020 Pouya Saadeghi – https://daisyui.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+*/
diff --git a/assets/vendor/heroicons.js b/assets/vendor/heroicons.js
@@ -0,0 +1,43 @@
+const plugin = require("tailwindcss/plugin")
+const fs = require("fs")
+const path = require("path")
+
+module.exports = plugin(function({matchComponents, theme}) {
+ let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized")
+ let values = {}
+ let icons = [
+ ["", "/24/outline"],
+ ["-solid", "/24/solid"],
+ ["-mini", "/20/solid"],
+ ["-micro", "/16/solid"]
+ ]
+ icons.forEach(([suffix, dir]) => {
+ fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
+ let name = path.basename(file, ".svg") + suffix
+ values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
+ })
+ })
+ matchComponents({
+ "hero": ({name, fullPath}) => {
+ let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
+ content = encodeURIComponent(content)
+ let size = theme("spacing.6")
+ if (name.endsWith("-mini")) {
+ size = theme("spacing.5")
+ } else if (name.endsWith("-micro")) {
+ size = theme("spacing.4")
+ }
+ return {
+ [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
+ "-webkit-mask": `var(--hero-${name})`,
+ "mask": `var(--hero-${name})`,
+ "mask-repeat": "no-repeat",
+ "background-color": "currentColor",
+ "vertical-align": "middle",
+ "display": "inline-block",
+ "width": size,
+ "height": size
+ }
+ }
+ }, {values})
+})
diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js
@@ -0,0 +1,138 @@
+/**
+ * @license MIT
+ * topbar 3.0.0
+ * http://buunguyen.github.io/topbar
+ * Copyright (c) 2024 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function (delay) {
+ if (showing) return;
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ if (!canvas.parentElement) document.body.appendChild(canvas);
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/config/config.exs b/config/config.exs
@@ -0,0 +1,80 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :daily_tracker, :scopes,
+ user: [
+ default: true,
+ module: DailyTracker.Accounts.Scope,
+ assign_key: :current_scope,
+ access_path: [:user, :id],
+ schema_key: :user_id,
+ schema_type: :id,
+ schema_table: :users,
+ test_data_fixture: DailyTracker.AccountsFixtures,
+ test_setup_helper: :register_and_log_in_user
+ ]
+
+config :daily_tracker,
+ ecto_repos: [DailyTracker.Repo],
+ generators: [timestamp_type: :utc_datetime]
+
+# Configure the endpoint
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ url: [host: "localhost"],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: DailyTrackerWeb.ErrorHTML, json: DailyTrackerWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: DailyTracker.PubSub,
+ live_view: [signing_salt: "wLCa0CAG"]
+
+# Configure the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :daily_tracker, DailyTracker.Mailer, adapter: Swoosh.Adapters.Local
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.25.4",
+ path: System.get_env("MIX_ESBUILD_PATH"),
+ daily_tracker: [
+ args:
+ ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
+ ]
+
+# Configure tailwind (the version is required)
+config :tailwind,
+ version: "4.3.0",
+ path: System.get_env("TAILWIND_PATH") || "tailwindcss",
+ daily_tracker: [
+ args: ~w(
+ --input=assets/css/app.css
+ --output=priv/static/assets/css/app.css
+ ),
+ cd: Path.expand("..", __DIR__)
+ ]
+
+# Configure Elixir's Logger
+config :logger, :default_formatter,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
@@ -0,0 +1,92 @@
+import Config
+
+# Configure your database
+config :daily_tracker, DailyTracker.Repo,
+ username: "postgres",
+ password: "postgres",
+ hostname: "localhost",
+ database: "daily_tracker_dev",
+ stacktrace: true,
+ show_sensitive_data_on_connection_error: true,
+ pool_size: 10
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "jFwZh1qj3jHkHQOURKZvpURqMnUK7ITMcIxirWwqzXYG5kTpe0vf6+0bGlq7bsfw",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:daily_tracker, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:daily_tracker, ~w(--watch)]}
+ ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Reload browser tabs when matching files change.
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ live_reload: [
+ web_console_logger: true,
+ patterns: [
+ # Static assets, except user uploads
+ ~r"priv/static/(?!uploads/).*\.(js|css|png|jpeg|jpg|gif|svg)$",
+ # Gettext translations
+ ~r"priv/gettext/.*\.po$",
+ # Router, Controllers, LiveViews and LiveComponents
+ ~r"lib/daily_tracker_web/router\.ex$",
+ ~r"lib/daily_tracker_web/(controllers|live|components)/.*\.(ex|heex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :daily_tracker, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :default_formatter, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+config :phoenix_live_view,
+ # Include debug annotations and locations in rendered markup.
+ # Changing this configuration will require mix clean and a full recompile.
+ debug_heex_annotations: true,
+ debug_attributes: true,
+ # Enable helpful, but potentially expensive runtime checks
+ enable_expensive_runtime_checks: true
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
diff --git a/config/prod.exs b/config/prod.exs
@@ -0,0 +1,33 @@
+import Config
+
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix assets.deploy` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Force using SSL in production. This also sets the "strict-security-transport" header,
+# known as HSTS. If you have a health check endpoint, you may want to exclude it below.
+# Note `:force_ssl` is required to be set at compile-time.
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ force_ssl: [
+ rewrite_on: [:x_forwarded_proto],
+ exclude: [
+ # paths: ["/health"],
+ hosts: ["localhost", "127.0.0.1"]
+ ]
+ ]
+
+# Configure Swoosh API Client
+config :swoosh, api_client: Swoosh.ApiClient.Req
+
+# Disable Swoosh Local Memory Storage
+config :swoosh, local: false
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/config/runtime.exs b/config/runtime.exs
@@ -0,0 +1,117 @@
+import Config
+
+# Make Erlang's DNS resolver respect /etc/hosts
+:inet_db.set_lookup([:file, :dns])
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/daily_tracker start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :daily_tracker, DailyTrackerWeb.Endpoint, server: true
+end
+
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ http: [port: String.to_integer(System.get_env("PORT", "4000"))]
+
+if config_env() == :prod do
+ database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
+
+ config :daily_tracker, DailyTracker.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
+ # For machines with several cores, consider starting multiple pools of `pool_size`
+ # pool_count: 4,
+ socket_options: maybe_ipv6
+
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+
+ config :daily_tracker, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
+
+ config :daily_tracker, DailyTrackerWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0}
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :daily_tracker, DailyTrackerWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your config/prod.exs,
+ # ensuring no data is ever sent via http, always redirecting to https:
+ #
+ # config :daily_tracker, DailyTrackerWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+
+ config :daily_tracker, DailyTracker.Mailer,
+ adapter: Swoosh.Adapters.SMTP,
+ relay: System.get_env("MAIL_HOST", "mail.silasbrack.com"),
+ port: 25,
+ username: "silas@silasbrack.com",
+ password: System.get_env("MAIL_PASSWORD") ||
+ raise("environment variable MAIL_PASSWORD is missing."),
+ tls: :always,
+ ssl: false,
+ tls_options: [cacerts: :public_key.cacerts_get()],
+ auth: :always
+end
diff --git a/config/test.exs b/config/test.exs
@@ -0,0 +1,44 @@
+import Config
+
+# Only in tests, remove the complexity from the password hashing algorithm
+config :bcrypt_elixir, :log_rounds, 1
+
+# Configure your database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :daily_tracker, DailyTracker.Repo,
+ username: "postgres",
+ password: "postgres",
+ hostname: "localhost",
+ database: "daily_tracker_test#{System.get_env("MIX_TEST_PARTITION")}",
+ pool: Ecto.Adapters.SQL.Sandbox,
+ pool_size: System.schedulers_online() * 2
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :daily_tracker, DailyTrackerWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "hzi8SbeS6auLvYpOK+OIp7Qmb0GnR1/g80cd88Tg/P+qO1nQbjOhiUBtdXI3XBs9",
+ server: false
+
+# In test we don't send emails
+config :daily_tracker, DailyTracker.Mailer, adapter: Swoosh.Adapters.Test
+
+# Disable swoosh api client as it is only required for production adapters
+config :swoosh, :api_client, false
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Enable helpful, but potentially expensive runtime checks
+config :phoenix_live_view,
+ enable_expensive_runtime_checks: true
+
+# Sort query params output of verified routes for robust url comparisons
+config :phoenix,
+ sort_verified_routes_query_params: true
diff --git a/deps.nix b/deps.nix
@@ -0,0 +1,610 @@
+{ lib, beamPackages, overrides ? (x: y: {}) }:
+
+let
+ buildRebar3 = lib.makeOverridable beamPackages.buildRebar3;
+ buildMix = lib.makeOverridable beamPackages.buildMix;
+ buildErlangMk = lib.makeOverridable beamPackages.buildErlangMk;
+
+ self = packages // (overrides self packages);
+
+ packages = with beamPackages; with self; {
+ bandit = buildMix rec {
+ name = "bandit";
+ version = "1.11.1";
+
+ src = fetchHex {
+ pkg = "bandit";
+ version = "${version}";
+ sha256 = "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5";
+ };
+
+ beamDeps = [ hpax plug telemetry thousand_island websock ];
+ };
+
+ bcrypt_elixir = buildMix rec {
+ name = "bcrypt_elixir";
+ version = "3.3.2";
+
+ src = fetchHex {
+ pkg = "bcrypt_elixir";
+ version = "${version}";
+ sha256 = "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53";
+ };
+
+ beamDeps = [ comeonin elixir_make ];
+ };
+
+ cc_precompiler = buildMix rec {
+ name = "cc_precompiler";
+ version = "0.1.11";
+
+ src = fetchHex {
+ pkg = "cc_precompiler";
+ version = "${version}";
+ sha256 = "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9";
+ };
+
+ beamDeps = [ elixir_make ];
+ };
+
+ comeonin = buildMix rec {
+ name = "comeonin";
+ version = "5.5.1";
+
+ src = fetchHex {
+ pkg = "comeonin";
+ version = "${version}";
+ sha256 = "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb";
+ };
+
+ beamDeps = [];
+ };
+
+ db_connection = buildMix rec {
+ name = "db_connection";
+ version = "2.10.1";
+
+ src = fetchHex {
+ pkg = "db_connection";
+ version = "${version}";
+ sha256 = "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3";
+ };
+
+ beamDeps = [ telemetry ];
+ };
+
+ decimal = buildMix rec {
+ name = "decimal";
+ version = "3.1.1";
+
+ src = fetchHex {
+ pkg = "decimal";
+ version = "${version}";
+ sha256 = "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6";
+ };
+
+ beamDeps = [];
+ };
+
+ dns_cluster = buildMix rec {
+ name = "dns_cluster";
+ version = "0.2.0";
+
+ src = fetchHex {
+ pkg = "dns_cluster";
+ version = "${version}";
+ sha256 = "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240";
+ };
+
+ beamDeps = [];
+ };
+
+ ecto = buildMix rec {
+ name = "ecto";
+ version = "3.14.0";
+
+ src = fetchHex {
+ pkg = "ecto";
+ version = "${version}";
+ sha256 = "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3";
+ };
+
+ beamDeps = [ decimal jason telemetry ];
+ };
+
+ ecto_sql = buildMix rec {
+ name = "ecto_sql";
+ version = "3.14.0";
+
+ src = fetchHex {
+ pkg = "ecto_sql";
+ version = "${version}";
+ sha256 = "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949";
+ };
+
+ beamDeps = [ db_connection decimal ecto postgrex telemetry ];
+ };
+
+ elixir_make = buildMix rec {
+ name = "elixir_make";
+ version = "0.9.0";
+
+ src = fetchHex {
+ pkg = "elixir_make";
+ version = "${version}";
+ sha256 = "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb";
+ };
+
+ beamDeps = [];
+ };
+
+ esbuild = buildMix rec {
+ name = "esbuild";
+ version = "0.10.0";
+
+ src = fetchHex {
+ pkg = "esbuild";
+ version = "${version}";
+ sha256 = "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70";
+ };
+
+ beamDeps = [ jason ];
+ };
+
+ expo = buildMix rec {
+ name = "expo";
+ version = "1.1.1";
+
+ src = fetchHex {
+ pkg = "expo";
+ version = "${version}";
+ sha256 = "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640";
+ };
+
+ beamDeps = [];
+ };
+
+ file_system = buildMix rec {
+ name = "file_system";
+ version = "1.1.1";
+
+ src = fetchHex {
+ pkg = "file_system";
+ version = "${version}";
+ sha256 = "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d";
+ };
+
+ beamDeps = [];
+ };
+
+ finch = buildMix rec {
+ name = "finch";
+ version = "0.22.0";
+
+ src = fetchHex {
+ pkg = "finch";
+ version = "${version}";
+ sha256 = "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc";
+ };
+
+ beamDeps = [ mime mint nimble_options nimble_pool telemetry ];
+ };
+
+ fine = buildMix rec {
+ name = "fine";
+ version = "0.1.6";
+
+ src = fetchHex {
+ pkg = "fine";
+ version = "${version}";
+ sha256 = "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12";
+ };
+
+ beamDeps = [];
+ };
+
+ gen_smtp = buildRebar3 rec {
+ name = "gen_smtp";
+ version = "1.3.0";
+
+ src = fetchHex {
+ pkg = "gen_smtp";
+ version = "${version}";
+ sha256 = "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6";
+ };
+
+ beamDeps = [ ranch ];
+ };
+
+ gettext = buildMix rec {
+ name = "gettext";
+ version = "1.0.2";
+
+ src = fetchHex {
+ pkg = "gettext";
+ version = "${version}";
+ sha256 = "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3";
+ };
+
+ beamDeps = [ expo ];
+ };
+
+ hpax = buildMix rec {
+ name = "hpax";
+ version = "1.0.3";
+
+ src = fetchHex {
+ pkg = "hpax";
+ version = "${version}";
+ sha256 = "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a";
+ };
+
+ beamDeps = [];
+ };
+
+ idna = buildRebar3 rec {
+ name = "idna";
+ version = "7.1.0";
+
+ src = fetchHex {
+ pkg = "idna";
+ version = "${version}";
+ sha256 = "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f";
+ };
+
+ beamDeps = [];
+ };
+
+ jason = buildMix rec {
+ name = "jason";
+ version = "1.4.5";
+
+ src = fetchHex {
+ pkg = "jason";
+ version = "${version}";
+ sha256 = "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684";
+ };
+
+ beamDeps = [ decimal ];
+ };
+
+ lazy_html = buildMix rec {
+ name = "lazy_html";
+ version = "0.1.11";
+
+ src = fetchHex {
+ pkg = "lazy_html";
+ version = "${version}";
+ sha256 = "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b";
+ };
+
+ beamDeps = [ cc_precompiler elixir_make fine ];
+ };
+
+ mime = buildMix rec {
+ name = "mime";
+ version = "2.0.7";
+
+ src = fetchHex {
+ pkg = "mime";
+ version = "${version}";
+ sha256 = "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd";
+ };
+
+ beamDeps = [];
+ };
+
+ mint = buildMix rec {
+ name = "mint";
+ version = "1.8.0";
+
+ src = fetchHex {
+ pkg = "mint";
+ version = "${version}";
+ sha256 = "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849";
+ };
+
+ beamDeps = [ hpax ];
+ };
+
+ nimble_options = buildMix rec {
+ name = "nimble_options";
+ version = "1.1.1";
+
+ src = fetchHex {
+ pkg = "nimble_options";
+ version = "${version}";
+ sha256 = "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44";
+ };
+
+ beamDeps = [];
+ };
+
+ nimble_pool = buildMix rec {
+ name = "nimble_pool";
+ version = "1.1.0";
+
+ src = fetchHex {
+ pkg = "nimble_pool";
+ version = "${version}";
+ sha256 = "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a";
+ };
+
+ beamDeps = [];
+ };
+
+ phoenix = buildMix rec {
+ name = "phoenix";
+ version = "1.8.7";
+
+ src = fetchHex {
+ pkg = "phoenix";
+ version = "${version}";
+ sha256 = "47352f72d6ab31009ef77516b1b3a14745be97b54061fd458031b9d8294869d5";
+ };
+
+ beamDeps = [ bandit jason phoenix_pubsub phoenix_template plug plug_crypto telemetry websock_adapter ];
+ };
+
+ phoenix_ecto = buildMix rec {
+ name = "phoenix_ecto";
+ version = "4.7.0";
+
+ src = fetchHex {
+ pkg = "phoenix_ecto";
+ version = "${version}";
+ sha256 = "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc";
+ };
+
+ beamDeps = [ ecto phoenix_html plug postgrex ];
+ };
+
+ phoenix_html = buildMix rec {
+ name = "phoenix_html";
+ version = "4.3.0";
+
+ src = fetchHex {
+ pkg = "phoenix_html";
+ version = "${version}";
+ sha256 = "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad";
+ };
+
+ beamDeps = [];
+ };
+
+ phoenix_live_dashboard = buildMix rec {
+ name = "phoenix_live_dashboard";
+ version = "0.8.7";
+
+ src = fetchHex {
+ pkg = "phoenix_live_dashboard";
+ version = "${version}";
+ sha256 = "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7";
+ };
+
+ beamDeps = [ ecto mime phoenix_live_view telemetry_metrics ];
+ };
+
+ phoenix_live_reload = buildMix rec {
+ name = "phoenix_live_reload";
+ version = "1.6.2";
+
+ src = fetchHex {
+ pkg = "phoenix_live_reload";
+ version = "${version}";
+ sha256 = "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9";
+ };
+
+ beamDeps = [ file_system phoenix ];
+ };
+
+ phoenix_live_view = buildMix rec {
+ name = "phoenix_live_view";
+ version = "1.1.31";
+
+ src = fetchHex {
+ pkg = "phoenix_live_view";
+ version = "${version}";
+ sha256 = "2f53cc6a9e149f30449341c2775990819d97e3b22338fe719c4d30342e6f9638";
+ };
+
+ beamDeps = [ jason lazy_html phoenix phoenix_html phoenix_template plug telemetry ];
+ };
+
+ phoenix_pubsub = buildMix rec {
+ name = "phoenix_pubsub";
+ version = "2.2.0";
+
+ src = fetchHex {
+ pkg = "phoenix_pubsub";
+ version = "${version}";
+ sha256 = "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96";
+ };
+
+ beamDeps = [];
+ };
+
+ phoenix_template = buildMix rec {
+ name = "phoenix_template";
+ version = "1.0.4";
+
+ src = fetchHex {
+ pkg = "phoenix_template";
+ version = "${version}";
+ sha256 = "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206";
+ };
+
+ beamDeps = [ phoenix_html ];
+ };
+
+ plug = buildMix rec {
+ name = "plug";
+ version = "1.19.2";
+
+ src = fetchHex {
+ pkg = "plug";
+ version = "${version}";
+ sha256 = "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0";
+ };
+
+ beamDeps = [ mime plug_crypto telemetry ];
+ };
+
+ plug_crypto = buildMix rec {
+ name = "plug_crypto";
+ version = "2.1.1";
+
+ src = fetchHex {
+ pkg = "plug_crypto";
+ version = "${version}";
+ sha256 = "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c";
+ };
+
+ beamDeps = [];
+ };
+
+ postgrex = buildMix rec {
+ name = "postgrex";
+ version = "0.22.2";
+
+ src = fetchHex {
+ pkg = "postgrex";
+ version = "${version}";
+ sha256 = "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b";
+ };
+
+ beamDeps = [ db_connection decimal jason ];
+ };
+
+ ranch = buildRebar3 rec {
+ name = "ranch";
+ version = "2.2.0";
+
+ src = fetchHex {
+ pkg = "ranch";
+ version = "${version}";
+ sha256 = "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7";
+ };
+
+ beamDeps = [];
+ };
+
+ req = buildMix rec {
+ name = "req";
+ version = "0.5.18";
+
+ src = fetchHex {
+ pkg = "req";
+ version = "${version}";
+ sha256 = "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996";
+ };
+
+ beamDeps = [ finch jason mime plug ];
+ };
+
+ swoosh = buildMix rec {
+ name = "swoosh";
+ version = "1.26.0";
+
+ src = fetchHex {
+ pkg = "swoosh";
+ version = "${version}";
+ sha256 = "83fe2c7c6572d21d5cdb47766ea041c13da234d09c96220c480eb90b21b1335c";
+ };
+
+ beamDeps = [ bandit finch gen_smtp idna jason mime plug req telemetry ];
+ };
+
+ tailwind = buildMix rec {
+ name = "tailwind";
+ version = "0.4.1";
+
+ src = fetchHex {
+ pkg = "tailwind";
+ version = "${version}";
+ sha256 = "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1";
+ };
+
+ beamDeps = [];
+ };
+
+ telemetry = buildRebar3 rec {
+ name = "telemetry";
+ version = "1.4.2";
+
+ src = fetchHex {
+ pkg = "telemetry";
+ version = "${version}";
+ sha256 = "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1";
+ };
+
+ beamDeps = [];
+ };
+
+ telemetry_metrics = buildMix rec {
+ name = "telemetry_metrics";
+ version = "1.1.0";
+
+ src = fetchHex {
+ pkg = "telemetry_metrics";
+ version = "${version}";
+ sha256 = "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67";
+ };
+
+ beamDeps = [ telemetry ];
+ };
+
+ telemetry_poller = buildRebar3 rec {
+ name = "telemetry_poller";
+ version = "1.3.0";
+
+ src = fetchHex {
+ pkg = "telemetry_poller";
+ version = "${version}";
+ sha256 = "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211";
+ };
+
+ beamDeps = [ telemetry ];
+ };
+
+ thousand_island = buildMix rec {
+ name = "thousand_island";
+ version = "1.4.3";
+
+ src = fetchHex {
+ pkg = "thousand_island";
+ version = "${version}";
+ sha256 = "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b";
+ };
+
+ beamDeps = [ telemetry ];
+ };
+
+ websock = buildMix rec {
+ name = "websock";
+ version = "0.5.3";
+
+ src = fetchHex {
+ pkg = "websock";
+ version = "${version}";
+ sha256 = "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453";
+ };
+
+ beamDeps = [];
+ };
+
+ websock_adapter = buildMix rec {
+ name = "websock_adapter";
+ version = "0.5.9";
+
+ src = fetchHex {
+ pkg = "websock_adapter";
+ version = "${version}";
+ sha256 = "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08";
+ };
+
+ beamDeps = [ bandit plug websock ];
+ };
+ };
+in self
+
diff --git a/esbuild b/esbuild
Binary files differ.
diff --git a/flake.nix b/flake.nix
@@ -0,0 +1,47 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
+ };
+
+ outputs =
+ { nixpkgs, ... }:
+ let
+ system = "x86_64-linux";
+ pkgs = import nixpkgs { inherit system; };
+ beamPackages = pkgs.beamPackages;
+
+ mixNixDeps = import ./deps.nix {
+ inherit (pkgs) lib;
+ inherit beamPackages;
+ };
+
+ heroicons = pkgs.fetchFromGitHub {
+ owner = "tailwindlabs";
+ repo = "heroicons";
+ rev = "v2.2.0";
+ sha256 = "sha256-Jcxr1fSbmXO9bZKeg39Z/zVN0YJp17TX3LH5Us4lsZU=";
+ };
+ in
+ {
+ packages.${system}.default = beamPackages.mixRelease {
+ pname = "daily_tracker";
+ version = "0.1.0";
+ src = ./.;
+
+ inherit mixNixDeps;
+
+ # Handle the heroicons git dep that mix2nix can't generate
+ postConfigure = ''
+ mkdir -p deps/heroicons
+ cp -r ${heroicons}/optimized/* deps/heroicons/
+ '';
+
+ # Build assets before release
+ preBuild = ''
+ export MIX_ESBUILD_PATH="${pkgs.esbuild}/bin/esbuild"
+ export TAILWIND_PATH="${pkgs.tailwindcss_4}/bin/tailwindcss"
+ mix assets.deploy --no-deps-check
+ '';
+ };
+ };
+}
diff --git a/lib/daily_tracker.ex b/lib/daily_tracker.ex
@@ -0,0 +1,9 @@
+defmodule DailyTracker do
+ @moduledoc """
+ DailyTracker keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/lib/daily_tracker/accounts.ex b/lib/daily_tracker/accounts.ex
@@ -0,0 +1,297 @@
+defmodule DailyTracker.Accounts do
+ @moduledoc """
+ The Accounts context.
+ """
+
+ import Ecto.Query, warn: false
+ alias DailyTracker.Repo
+
+ alias DailyTracker.Accounts.{User, UserToken, UserNotifier}
+
+ ## Database getters
+
+ @doc """
+ Gets a user by email.
+
+ ## Examples
+
+ iex> get_user_by_email("foo@example.com")
+ %User{}
+
+ iex> get_user_by_email("unknown@example.com")
+ nil
+
+ """
+ def get_user_by_email(email) when is_binary(email) do
+ Repo.get_by(User, email: email)
+ end
+
+ @doc """
+ Gets a user by email and password.
+
+ ## Examples
+
+ iex> get_user_by_email_and_password("foo@example.com", "correct_password")
+ %User{}
+
+ iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
+ nil
+
+ """
+ def get_user_by_email_and_password(email, password)
+ when is_binary(email) and is_binary(password) do
+ user = Repo.get_by(User, email: email)
+ if User.valid_password?(user, password), do: user
+ end
+
+ @doc """
+ Gets a single user.
+
+ Raises `Ecto.NoResultsError` if the User does not exist.
+
+ ## Examples
+
+ iex> get_user!(123)
+ %User{}
+
+ iex> get_user!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_user!(id), do: Repo.get!(User, id)
+
+ ## User registration
+
+ @doc """
+ Registers a user.
+
+ ## Examples
+
+ iex> register_user(%{field: value})
+ {:ok, %User{}}
+
+ iex> register_user(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def register_user(attrs) do
+ %User{}
+ |> User.email_changeset(attrs)
+ |> Repo.insert()
+ end
+
+ ## Settings
+
+ @doc """
+ Checks whether the user is in sudo mode.
+
+ The user is in sudo mode when the last authentication was done no further
+ than 20 minutes ago. The limit can be given as second argument in minutes.
+ """
+ def sudo_mode?(user, minutes \\ -20)
+
+ def sudo_mode?(%User{authenticated_at: ts}, minutes) when is_struct(ts, DateTime) do
+ DateTime.after?(ts, DateTime.utc_now() |> DateTime.add(minutes, :minute))
+ end
+
+ def sudo_mode?(_user, _minutes), do: false
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for changing the user email.
+
+ See `DailyTracker.Accounts.User.email_changeset/3` for a list of supported options.
+
+ ## Examples
+
+ iex> change_user_email(user)
+ %Ecto.Changeset{data: %User{}}
+
+ """
+ def change_user_email(user, attrs \\ %{}, opts \\ []) do
+ User.email_changeset(user, attrs, opts)
+ end
+
+ @doc """
+ Updates the user email using the given token.
+
+ If the token matches, the user email is updated and the token is deleted.
+ """
+ def update_user_email(user, token) do
+ context = "change:#{user.email}"
+
+ Repo.transact(fn ->
+ with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
+ %UserToken{sent_to: email} <- Repo.one(query),
+ {:ok, user} <- Repo.update(User.email_changeset(user, %{email: email})),
+ {_count, _result} <-
+ Repo.delete_all(from(UserToken, where: [user_id: ^user.id, context: ^context])) do
+ {:ok, user}
+ else
+ _ -> {:error, :transaction_aborted}
+ end
+ end)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for changing the user password.
+
+ See `DailyTracker.Accounts.User.password_changeset/3` for a list of supported options.
+
+ ## Examples
+
+ iex> change_user_password(user)
+ %Ecto.Changeset{data: %User{}}
+
+ """
+ def change_user_password(user, attrs \\ %{}, opts \\ []) do
+ User.password_changeset(user, attrs, opts)
+ end
+
+ @doc """
+ Updates the user password.
+
+ Returns a tuple with the updated user, as well as a list of expired tokens.
+
+ ## Examples
+
+ iex> update_user_password(user, %{password: ...})
+ {:ok, {%User{}, [...]}}
+
+ iex> update_user_password(user, %{password: "too short"})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_user_password(user, attrs) do
+ user
+ |> User.password_changeset(attrs)
+ |> update_user_and_delete_all_tokens()
+ end
+
+ ## Session
+
+ @doc """
+ Generates a session token.
+ """
+ def generate_user_session_token(user) do
+ {token, user_token} = UserToken.build_session_token(user)
+ Repo.insert!(user_token)
+ token
+ end
+
+ @doc """
+ Gets the user with the given signed token.
+
+ If the token is valid `{user, token_inserted_at}` is returned, otherwise `nil` is returned.
+ """
+ def get_user_by_session_token(token) do
+ {:ok, query} = UserToken.verify_session_token_query(token)
+ Repo.one(query)
+ end
+
+ @doc """
+ Gets the user with the given magic link token.
+ """
+ def get_user_by_magic_link_token(token) do
+ with {:ok, query} <- UserToken.verify_magic_link_token_query(token),
+ {user, _token} <- Repo.one(query) do
+ user
+ else
+ _ -> nil
+ end
+ end
+
+ @doc """
+ Logs the user in by magic link.
+
+ There are three cases to consider:
+
+ 1. The user has already confirmed their email. They are logged in
+ and the magic link is expired.
+
+ 2. The user has not confirmed their email and no password is set.
+ In this case, the user gets confirmed, logged in, and all tokens -
+ including session ones - are expired. In theory, no other tokens
+ exist but we delete all of them for best security practices.
+
+ 3. The user has not confirmed their email but a password is set.
+ This cannot happen in the default implementation but may be the
+ source of security pitfalls. See the "Mixing magic link and password registration" section of
+ `mix help phx.gen.auth`.
+ """
+ def login_user_by_magic_link(token) do
+ {:ok, query} = UserToken.verify_magic_link_token_query(token)
+
+ case Repo.one(query) do
+ # Prevent session fixation attacks by disallowing magic links for unconfirmed users with password
+ {%User{confirmed_at: nil, hashed_password: hash}, _token} when not is_nil(hash) ->
+ raise """
+ magic link log in is not allowed for unconfirmed users with a password set!
+
+ This cannot happen with the default implementation, which indicates that you
+ might have adapted the code to a different use case. Please make sure to read the
+ "Mixing magic link and password registration" section of `mix help phx.gen.auth`.
+ """
+
+ {%User{confirmed_at: nil} = user, _token} ->
+ user
+ |> User.confirm_changeset()
+ |> update_user_and_delete_all_tokens()
+
+ {user, token} ->
+ Repo.delete!(token)
+ {:ok, {user, []}}
+
+ nil ->
+ {:error, :not_found}
+ end
+ end
+
+ @doc ~S"""
+ Delivers the update email instructions to the given user.
+
+ ## Examples
+
+ iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm-email/#{&1}"))
+ {:ok, %{to: ..., body: ...}}
+
+ """
+ def deliver_user_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
+ when is_function(update_email_url_fun, 1) do
+ {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
+
+ Repo.insert!(user_token)
+ UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
+ end
+
+ @doc """
+ Delivers the magic link login instructions to the given user.
+ """
+ def deliver_login_instructions(%User{} = user, magic_link_url_fun)
+ when is_function(magic_link_url_fun, 1) do
+ {encoded_token, user_token} = UserToken.build_email_token(user, "login")
+ Repo.insert!(user_token)
+ UserNotifier.deliver_login_instructions(user, magic_link_url_fun.(encoded_token))
+ end
+
+ @doc """
+ Deletes the signed token with the given context.
+ """
+ def delete_user_session_token(token) do
+ Repo.delete_all(from(UserToken, where: [token: ^token, context: "session"]))
+ :ok
+ end
+
+ ## Token helper
+
+ defp update_user_and_delete_all_tokens(changeset) do
+ Repo.transact(fn ->
+ with {:ok, user} <- Repo.update(changeset) do
+ tokens_to_expire = Repo.all_by(UserToken, user_id: user.id)
+
+ Repo.delete_all(from(t in UserToken, where: t.id in ^Enum.map(tokens_to_expire, & &1.id)))
+
+ {:ok, {user, tokens_to_expire}}
+ end
+ end)
+ end
+end
diff --git a/lib/daily_tracker/accounts/scope.ex b/lib/daily_tracker/accounts/scope.ex
@@ -0,0 +1,33 @@
+defmodule DailyTracker.Accounts.Scope do
+ @moduledoc """
+ Defines the scope of the caller to be used throughout the app.
+
+ The `DailyTracker.Accounts.Scope` allows public interfaces to receive
+ information about the caller, such as if the call is initiated from an
+ end-user, and if so, which user. Additionally, such a scope can carry fields
+ such as "super user" or other privileges for use as authorization, or to
+ ensure specific code paths can only be access for a given scope.
+
+ It is useful for logging as well as for scoping pubsub subscriptions and
+ broadcasts when a caller subscribes to an interface or performs a particular
+ action.
+
+ Feel free to extend the fields on this struct to fit the needs of
+ growing application requirements.
+ """
+
+ alias DailyTracker.Accounts.User
+
+ defstruct user: nil
+
+ @doc """
+ Creates a scope for the given user.
+
+ Returns nil if no user is given.
+ """
+ def for_user(%User{} = user) do
+ %__MODULE__{user: user}
+ end
+
+ def for_user(nil), do: nil
+end
diff --git a/lib/daily_tracker/accounts/user.ex b/lib/daily_tracker/accounts/user.ex
@@ -0,0 +1,132 @@
+defmodule DailyTracker.Accounts.User do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "users" do
+ field :email, :string
+ field :password, :string, virtual: true, redact: true
+ field :hashed_password, :string, redact: true
+ field :confirmed_at, :utc_datetime
+ field :authenticated_at, :utc_datetime, virtual: true
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc """
+ A user changeset for registering or changing the email.
+
+ It requires the email to change otherwise an error is added.
+
+ ## Options
+
+ * `:validate_unique` - Set to false if you don't want to validate the
+ uniqueness of the email, useful when displaying live validations.
+ Defaults to `true`.
+ """
+ def email_changeset(user, attrs, opts \\ []) do
+ user
+ |> cast(attrs, [:email])
+ |> validate_email(opts)
+ end
+
+ defp validate_email(changeset, opts) do
+ changeset =
+ changeset
+ |> validate_required([:email])
+ |> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/,
+ message: "must have the @ sign and no spaces"
+ )
+ |> validate_length(:email, max: 160)
+
+ if Keyword.get(opts, :validate_unique, true) do
+ changeset
+ |> unsafe_validate_unique(:email, DailyTracker.Repo)
+ |> unique_constraint(:email)
+ |> validate_email_changed()
+ else
+ changeset
+ end
+ end
+
+ defp validate_email_changed(changeset) do
+ if get_field(changeset, :email) && get_change(changeset, :email) == nil do
+ add_error(changeset, :email, "did not change")
+ else
+ changeset
+ end
+ end
+
+ @doc """
+ A user changeset for changing the password.
+
+ It is important to validate the length of the password, as long passwords may
+ be very expensive to hash for certain algorithms.
+
+ ## Options
+
+ * `:hash_password` - Hashes the password so it can be stored securely
+ in the database and ensures the password field is cleared to prevent
+ leaks in the logs. If password hashing is not needed and clearing the
+ password field is not desired (like when using this changeset for
+ validations on a LiveView form), this option can be set to `false`.
+ Defaults to `true`.
+ """
+ def password_changeset(user, attrs, opts \\ []) do
+ user
+ |> cast(attrs, [:password])
+ |> validate_confirmation(:password, message: "does not match password")
+ |> validate_password(opts)
+ end
+
+ defp validate_password(changeset, opts) do
+ changeset
+ |> validate_required([:password])
+ |> validate_length(:password, min: 12, max: 72)
+ # Examples of additional password validation:
+ # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
+ # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
+ # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
+ |> maybe_hash_password(opts)
+ end
+
+ defp maybe_hash_password(changeset, opts) do
+ hash_password? = Keyword.get(opts, :hash_password, true)
+ password = get_change(changeset, :password)
+
+ if hash_password? && password && changeset.valid? do
+ changeset
+ # If using Bcrypt, then further validate it is at most 72 bytes long
+ |> validate_length(:password, max: 72, count: :bytes)
+ # Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
+ # would keep the database transaction open longer and hurt performance.
+ |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
+ |> delete_change(:password)
+ else
+ changeset
+ end
+ end
+
+ @doc """
+ Confirms the account by setting `confirmed_at`.
+ """
+ def confirm_changeset(user) do
+ now = DateTime.utc_now(:second)
+ change(user, confirmed_at: now)
+ end
+
+ @doc """
+ Verifies the password.
+
+ If there is no user or the user doesn't have a password, we call
+ `Bcrypt.no_user_verify/0` to avoid timing attacks.
+ """
+ def valid_password?(%DailyTracker.Accounts.User{hashed_password: hashed_password}, password)
+ when is_binary(hashed_password) and byte_size(password) > 0 do
+ Bcrypt.verify_pass(password, hashed_password)
+ end
+
+ def valid_password?(_, _) do
+ Bcrypt.no_user_verify()
+ false
+ end
+end
diff --git a/lib/daily_tracker/accounts/user_notifier.ex b/lib/daily_tracker/accounts/user_notifier.ex
@@ -0,0 +1,84 @@
+defmodule DailyTracker.Accounts.UserNotifier do
+ import Swoosh.Email
+
+ alias DailyTracker.Mailer
+ alias DailyTracker.Accounts.User
+
+ # Delivers the email using the application mailer.
+ defp deliver(recipient, subject, body) do
+ email =
+ new()
+ |> to(recipient)
+ |> from({"DailyTracker", "silas@silasbrack.com"})
+ |> subject(subject)
+ |> text_body(body)
+
+ with {:ok, _metadata} <- Mailer.deliver(email) do
+ {:ok, email}
+ end
+ end
+
+ @doc """
+ Deliver instructions to update a user email.
+ """
+ def deliver_update_email_instructions(user, url) do
+ deliver(user.email, "Update email instructions", """
+
+ ==============================
+
+ Hi #{user.email},
+
+ You can change your email by visiting the URL below:
+
+ #{url}
+
+ If you didn't request this change, please ignore this.
+
+ ==============================
+ """)
+ end
+
+ @doc """
+ Deliver instructions to log in with a magic link.
+ """
+ def deliver_login_instructions(user, url) do
+ case user do
+ %User{confirmed_at: nil} -> deliver_confirmation_instructions(user, url)
+ _ -> deliver_magic_link_instructions(user, url)
+ end
+ end
+
+ defp deliver_magic_link_instructions(user, url) do
+ deliver(user.email, "Log in instructions", """
+
+ ==============================
+
+ Hi #{user.email},
+
+ You can log into your account by visiting the URL below:
+
+ #{url}
+
+ If you didn't request this email, please ignore this.
+
+ ==============================
+ """)
+ end
+
+ defp deliver_confirmation_instructions(user, url) do
+ deliver(user.email, "Confirmation instructions", """
+
+ ==============================
+
+ Hi #{user.email},
+
+ You can confirm your account by visiting the URL below:
+
+ #{url}
+
+ If you didn't create an account with us, please ignore this.
+
+ ==============================
+ """)
+ end
+end
diff --git a/lib/daily_tracker/accounts/user_token.ex b/lib/daily_tracker/accounts/user_token.ex
@@ -0,0 +1,156 @@
+defmodule DailyTracker.Accounts.UserToken do
+ use Ecto.Schema
+ import Ecto.Query
+ alias DailyTracker.Accounts.UserToken
+
+ @hash_algorithm :sha256
+ @rand_size 32
+
+ # It is very important to keep the magic link token expiry short,
+ # since someone with access to the email may take over the account.
+ @magic_link_validity_in_minutes 15
+ @change_email_validity_in_days 7
+ @session_validity_in_days 14
+
+ schema "users_tokens" do
+ field :token, :binary
+ field :context, :string
+ field :sent_to, :string
+ field :authenticated_at, :utc_datetime
+ belongs_to :user, DailyTracker.Accounts.User
+
+ timestamps(type: :utc_datetime, updated_at: false)
+ end
+
+ @doc """
+ Generates a token that will be stored in a signed place,
+ such as session or cookie. As they are signed, those
+ tokens do not need to be hashed.
+
+ The reason why we store session tokens in the database, even
+ though Phoenix already provides a session cookie, is because
+ Phoenix's default session cookies are not persisted, they are
+ simply signed and potentially encrypted. This means they are
+ valid indefinitely, unless you change the signing/encryption
+ salt.
+
+ Therefore, storing them allows individual user
+ sessions to be expired. The token system can also be extended
+ to store additional data, such as the device used for logging in.
+ You could then use this information to display all valid sessions
+ and devices in the UI and allow users to explicitly expire any
+ session they deem invalid.
+ """
+ def build_session_token(user) do
+ token = :crypto.strong_rand_bytes(@rand_size)
+ dt = user.authenticated_at || DateTime.utc_now(:second)
+ {token, %UserToken{token: token, context: "session", user_id: user.id, authenticated_at: dt}}
+ end
+
+ @doc """
+ Checks if the token is valid and returns its underlying lookup query.
+
+ The query returns the user found by the token, if any, along with the token's creation time.
+
+ The token is valid if it matches the value in the database and it has
+ not expired (after @session_validity_in_days).
+ """
+ def verify_session_token_query(token) do
+ query =
+ from token in by_token_and_context_query(token, "session"),
+ join: user in assoc(token, :user),
+ where: token.inserted_at > ago(@session_validity_in_days, "day"),
+ select: {%{user | authenticated_at: token.authenticated_at}, token.inserted_at}
+
+ {:ok, query}
+ end
+
+ @doc """
+ Builds a token and its hash to be delivered to the user's email.
+
+ The non-hashed token is sent to the user email while the
+ hashed part is stored in the database. The original token cannot be reconstructed,
+ which means anyone with read-only access to the database cannot directly use
+ the token in the application to gain access. Furthermore, if the user changes
+ their email in the system, the tokens sent to the previous email are no longer
+ valid.
+
+ Users can easily adapt the existing code to provide other types of delivery methods,
+ for example, by phone numbers.
+ """
+ def build_email_token(user, context) do
+ build_hashed_token(user, context, user.email)
+ end
+
+ defp build_hashed_token(user, context, sent_to) do
+ token = :crypto.strong_rand_bytes(@rand_size)
+ hashed_token = :crypto.hash(@hash_algorithm, token)
+
+ {Base.url_encode64(token, padding: false),
+ %UserToken{
+ token: hashed_token,
+ context: context,
+ sent_to: sent_to,
+ user_id: user.id
+ }}
+ end
+
+ @doc """
+ Checks if the token is valid and returns its underlying lookup query.
+
+ If found, the query returns a tuple of the form `{user, token}`.
+
+ The given token is valid if it matches its hashed counterpart in the
+ database. This function also checks whether the token has expired. The context
+ of a magic link token is always "login".
+ """
+ def verify_magic_link_token_query(token) do
+ case Base.url_decode64(token, padding: false) do
+ {:ok, decoded_token} ->
+ hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
+
+ query =
+ from token in by_token_and_context_query(hashed_token, "login"),
+ join: user in assoc(token, :user),
+ where: token.inserted_at > ago(^@magic_link_validity_in_minutes, "minute"),
+ where: token.sent_to == user.email,
+ select: {user, token}
+
+ {:ok, query}
+
+ :error ->
+ :error
+ end
+ end
+
+ @doc """
+ Checks if the token is valid and returns its underlying lookup query.
+
+ The query returns the user_token found by the token, if any.
+
+ This is used to validate requests to change the user
+ email.
+ The given token is valid if it matches its hashed counterpart in the
+ database and if it has not expired (after @change_email_validity_in_days).
+ The context must always start with "change:".
+ """
+ def verify_change_email_token_query(token, "change:" <> _ = context) do
+ case Base.url_decode64(token, padding: false) do
+ {:ok, decoded_token} ->
+ hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
+
+ query =
+ from token in by_token_and_context_query(hashed_token, context),
+ where: token.inserted_at > ago(@change_email_validity_in_days, "day")
+
+ {:ok, query}
+
+ :error ->
+ :error
+ end
+ end
+
+ defp by_token_and_context_query(token, context) do
+ from UserToken, where: [token: ^token, context: ^context]
+ end
+end
diff --git a/lib/daily_tracker/application.ex b/lib/daily_tracker/application.ex
@@ -0,0 +1,34 @@
+defmodule DailyTracker.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ DailyTrackerWeb.Telemetry,
+ DailyTracker.Repo,
+ {DNSCluster, query: Application.get_env(:daily_tracker, :dns_cluster_query) || :ignore},
+ {Phoenix.PubSub, name: DailyTracker.PubSub},
+ # Start a worker by calling: DailyTracker.Worker.start_link(arg)
+ # {DailyTracker.Worker, arg},
+ # Start to serve requests, typically the last entry
+ DailyTrackerWeb.Endpoint
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: DailyTracker.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ DailyTrackerWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/lib/daily_tracker/mailer.ex b/lib/daily_tracker/mailer.ex
@@ -0,0 +1,3 @@
+defmodule DailyTracker.Mailer do
+ use Swoosh.Mailer, otp_app: :daily_tracker
+end
diff --git a/lib/daily_tracker/release.ex b/lib/daily_tracker/release.ex
@@ -0,0 +1,24 @@
+defmodule DailyTracker.Release do
+ @app :daily_tracker
+
+ def migrate do
+ load_app()
+
+ for repo <- repos() do
+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
+ end
+ end
+
+ def rollback(repo, version) do
+ load_app()
+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
+ end
+
+ defp repos do
+ Application.fetch_env!(@app, :ecto_repos)
+ end
+
+ defp load_app do
+ Application.load(@app)
+ end
+end
diff --git a/lib/daily_tracker/repo.ex b/lib/daily_tracker/repo.ex
@@ -0,0 +1,5 @@
+defmodule DailyTracker.Repo do
+ use Ecto.Repo,
+ otp_app: :daily_tracker,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/lib/daily_tracker/tracker.ex b/lib/daily_tracker/tracker.ex
@@ -0,0 +1,38 @@
+defmodule DailyTracker.Tracker do
+ import Ecto.Query, warn: false
+ alias DailyTracker.Repo
+ alias DailyTracker.Tracker.Entry
+ alias DailyTracker.Accounts.Scope
+
+ def list_entries(%Scope{user: user}) do
+ Entry
+ |> where(user_id: ^user.id)
+ |> Repo.all()
+ end
+
+ def get_entry!(%Scope{user: user}, id) do
+ Entry
+ |> where(user_id: ^user.id)
+ |> Repo.get!(id)
+ end
+
+ def create_entry(%Scope{user: user}, attrs) do
+ %Entry{user_id: user.id}
+ |> Entry.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ def update_entry(%Entry{} = entry, attrs) do
+ entry
+ |> Entry.changeset(attrs)
+ |> Repo.update()
+ end
+
+ def delete_entry(%Entry{} = entry) do
+ Repo.delete(entry)
+ end
+
+ def change_entry(%Entry{} = entry, attrs \\ %{}) do
+ Entry.changeset(entry, attrs)
+ end
+end
diff --git a/lib/daily_tracker/tracker/entry.ex b/lib/daily_tracker/tracker/entry.ex
@@ -0,0 +1,27 @@
+defmodule DailyTracker.Tracker.Entry do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "entries" do
+ field :date, :date
+ field :mood, :string
+ field :headache, :boolean, default: false
+ field :water_glasses, :integer
+ field :worked_out, :boolean, default: false
+ field :bowel_movement, :boolean, default: false
+ field :hunger_level, :integer
+ field :period, :boolean, default: false
+ field :notes, :string
+
+ belongs_to :user, DailyTracker.Accounts.User
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(entry, attrs) do
+ entry
+ |> cast(attrs, [:date, :mood, :headache, :water_glasses, :worked_out, :bowel_movement, :hunger_level, :period, :notes])
+ |> validate_required([:date, :mood, :headache, :water_glasses, :worked_out, :bowel_movement, :hunger_level, :period, :notes])
+ end
+end
diff --git a/lib/daily_tracker_web.ex b/lib/daily_tracker_web.ex
@@ -0,0 +1,114 @@
+defmodule DailyTrackerWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, components, channels, and so on.
+
+ This can be used in your application as:
+
+ use DailyTrackerWeb, :controller
+ use DailyTrackerWeb, :html
+
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define additional modules and import
+ those modules here.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller, formats: [:html, :json]
+
+ use Gettext, backend: DailyTrackerWeb.Gettext
+
+ import Plug.Conn
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # Translation
+ use Gettext, backend: DailyTrackerWeb.Gettext
+
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components
+ import DailyTrackerWeb.CoreComponents
+
+ # Common modules used in templates
+ alias Phoenix.LiveView.JS
+ alias DailyTrackerWeb.Layouts
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: DailyTrackerWeb.Endpoint,
+ router: DailyTrackerWeb.Router,
+ statics: DailyTrackerWeb.static_paths()
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/live_view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/lib/daily_tracker_web/components/core_components.ex b/lib/daily_tracker_web/components/core_components.ex
@@ -0,0 +1,505 @@
+defmodule DailyTrackerWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ At first glance, this module may seem daunting, but its goal is to provide
+ core building blocks for your application, such as tables, forms, and
+ inputs. The components consist mostly of markup and are well-documented
+ with doc strings and declarative assigns. You may customize and style
+ them in any way you want, based on your application growth and needs.
+
+ The foundation for styling is Tailwind CSS, a utility-first CSS framework,
+ augmented with daisyUI, a Tailwind CSS plugin that provides UI components
+ and themes. Here are useful references:
+
+ * [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
+ started and see the available components.
+
+ * [Tailwind CSS](https://tailwindcss.com) - the foundational framework
+ we build on. You will use it for layout, sizing, flexbox, grid, and
+ spacing.
+
+ * [Heroicons](https://heroicons.com) - see `icon/1` for usage.
+
+ * [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) -
+ the component system used by Phoenix. Some components, such as `<.link>`
+ and `<.form>`, are defined there.
+
+ """
+ use Phoenix.Component
+ use Gettext, backend: DailyTrackerWeb.Gettext
+
+ alias Phoenix.LiveView.JS
+
+ @doc """
+ Renders flash notices.
+
+ ## Examples
+
+ <.flash kind={:info} flash={@flash} />
+ <.flash
+ id="welcome-back"
+ kind={:info}
+ phx-mounted={show("#welcome-back") |> JS.remove_attribute("hidden")}
+ hidden
+ >
+ Welcome Back!
+ </.flash>
+ """
+ attr :id, :string, doc: "the optional id of flash container"
+ attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
+ attr :title, :string, default: nil
+ attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
+ attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
+
+ slot :inner_block, doc: "the optional inner block that renders the flash message"
+
+ def flash(assigns) do
+ assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
+
+ ~H"""
+ <div
+ :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
+ id={@id}
+ phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
+ role="alert"
+ class="toast toast-top toast-end z-50"
+ {@rest}
+ >
+ <div class={[
+ "alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
+ @kind == :info && "alert-info",
+ @kind == :error && "alert-error"
+ ]}>
+ <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
+ <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
+ <div>
+ <p :if={@title} class="font-semibold">{@title}</p>
+ <p>{msg}</p>
+ </div>
+ <div class="flex-1" />
+ <button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
+ <.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
+ </button>
+ </div>
+ </div>
+ """
+ end
+
+ @doc """
+ Renders a button with navigation support.
+
+ ## Examples
+
+ <.button>Send!</.button>
+ <.button phx-click="go" variant="primary">Send!</.button>
+ <.button navigate={~p"/"}>Home</.button>
+ """
+ attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
+ attr :class, :any
+ attr :variant, :string, values: ~w(primary)
+ slot :inner_block, required: true
+
+ def button(%{rest: rest} = assigns) do
+ variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
+
+ assigns =
+ assign_new(assigns, :class, fn ->
+ ["btn", Map.fetch!(variants, assigns[:variant])]
+ end)
+
+ if rest[:href] || rest[:navigate] || rest[:patch] do
+ ~H"""
+ <.link class={@class} {@rest}>
+ {render_slot(@inner_block)}
+ </.link>
+ """
+ else
+ ~H"""
+ <button class={@class} {@rest}>
+ {render_slot(@inner_block)}
+ </button>
+ """
+ end
+ end
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `Phoenix.HTML.FormField` may be passed as argument,
+ which is used to retrieve the input name, id, and values.
+ Otherwise all attributes may be passed explicitly.
+
+ ## Types
+
+ This function accepts all HTML input types, considering that:
+
+ * You may also set `type="select"` to render a `<select>` tag
+
+ * `type="checkbox"` is used exclusively to render boolean values
+
+ * For live file uploads, see `Phoenix.Component.live_file_input/1`
+
+ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
+ for more information. Unsupported types, such as radio, are best
+ written directly in your templates.
+
+ ## Examples
+
+ ```heex
+ <.input field={@form[:email]} type="email" />
+ <.input name="my-input" errors={["oh no!"]} />
+ ```
+
+ ## Select type
+
+ When using `type="select"`, you must pass the `options` and optionally
+ a `value` to mark which option should be preselected.
+
+ ```heex
+ <.input field={@form[:user_type]} type="select" options={["Admin": "admin", "User": "user"]} />
+ ```
+
+ For more information on what kind of data can be passed to `options` see
+ [`options_for_select`](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#options_for_select/2).
+ """
+ attr :id, :any, default: nil
+ attr :name, :any
+ attr :label, :string, default: nil
+ attr :value, :any
+
+ attr :type, :string,
+ default: "text",
+ values: ~w(checkbox color date datetime-local email file month number password
+ search select tel text textarea time url week hidden)
+
+ attr :field, Phoenix.HTML.FormField,
+ doc: "a form field struct retrieved from the form, for example: @form[:email]"
+
+ attr :errors, :list, default: []
+ attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
+ attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
+ attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
+ attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
+ attr :class, :any, default: nil, doc: "the input class to use over defaults"
+ attr :error_class, :any, default: nil, doc: "the input error class to use over defaults"
+
+ attr :rest, :global,
+ include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
+ multiple pattern placeholder readonly required rows size step)
+
+ def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
+ errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
+
+ assigns
+ |> assign(field: nil, id: assigns.id || field.id)
+ |> assign(:errors, Enum.map(errors, &translate_error(&1)))
+ |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+ |> assign_new(:value, fn -> field.value end)
+ |> input()
+ end
+
+ def input(%{type: "hidden"} = assigns) do
+ ~H"""
+ <input type="hidden" id={@id} name={@name} value={@value} {@rest} />
+ """
+ end
+
+ def input(%{type: "checkbox"} = assigns) do
+ assigns =
+ assign_new(assigns, :checked, fn ->
+ Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
+ end)
+
+ ~H"""
+ <div class="fieldset mb-2">
+ <label for={@id}>
+ <input
+ type="hidden"
+ name={@name}
+ value="false"
+ disabled={@rest[:disabled]}
+ form={@rest[:form]}
+ />
+ <span class="label">
+ <input
+ type="checkbox"
+ id={@id}
+ name={@name}
+ value="true"
+ checked={@checked}
+ class={@class || "checkbox checkbox-sm"}
+ {@rest}
+ />{@label}
+ </span>
+ </label>
+ <.error :for={msg <- @errors}>{msg}</.error>
+ </div>
+ """
+ end
+
+ def input(%{type: "select"} = assigns) do
+ ~H"""
+ <div class="fieldset mb-2">
+ <label for={@id}>
+ <span :if={@label} class="label mb-1">{@label}</span>
+ <select
+ id={@id}
+ name={@name}
+ class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
+ multiple={@multiple}
+ {@rest}
+ >
+ <option :if={@prompt} value="">{@prompt}</option>
+ {Phoenix.HTML.Form.options_for_select(@options, @value)}
+ </select>
+ </label>
+ <.error :for={msg <- @errors}>{msg}</.error>
+ </div>
+ """
+ end
+
+ def input(%{type: "textarea"} = assigns) do
+ ~H"""
+ <div class="fieldset mb-2">
+ <label for={@id}>
+ <span :if={@label} class="label mb-1">{@label}</span>
+ <textarea
+ id={@id}
+ name={@name}
+ class={[
+ @class || "w-full textarea",
+ @errors != [] && (@error_class || "textarea-error")
+ ]}
+ {@rest}
+ >{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
+ </label>
+ <.error :for={msg <- @errors}>{msg}</.error>
+ </div>
+ """
+ end
+
+ # All other inputs text, datetime-local, url, password, etc. are handled here...
+ def input(assigns) do
+ ~H"""
+ <div class="fieldset mb-2">
+ <label for={@id}>
+ <span :if={@label} class="label mb-1">{@label}</span>
+ <input
+ type={@type}
+ name={@name}
+ id={@id}
+ value={Phoenix.HTML.Form.normalize_value(@type, @value)}
+ class={[
+ @class || "w-full input",
+ @errors != [] && (@error_class || "input-error")
+ ]}
+ {@rest}
+ />
+ </label>
+ <.error :for={msg <- @errors}>{msg}</.error>
+ </div>
+ """
+ end
+
+ # Helper used by inputs to generate form errors
+ defp error(assigns) do
+ ~H"""
+ <p class="mt-1.5 flex gap-2 items-center text-sm text-error">
+ <.icon name="hero-exclamation-circle" class="size-5" />
+ {render_slot(@inner_block)}
+ </p>
+ """
+ end
+
+ @doc """
+ Renders a header with title.
+ """
+ slot :inner_block, required: true
+ slot :subtitle
+ slot :actions
+
+ def header(assigns) do
+ ~H"""
+ <header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
+ <div>
+ <h1 class="text-lg font-semibold leading-8">
+ {render_slot(@inner_block)}
+ </h1>
+ <p :if={@subtitle != []} class="text-sm text-base-content/70">
+ {render_slot(@subtitle)}
+ </p>
+ </div>
+ <div class="flex-none">{render_slot(@actions)}</div>
+ </header>
+ """
+ end
+
+ @doc """
+ Renders a table with generic styling.
+
+ ## Examples
+
+ <.table id="users" rows={@users}>
+ <:col :let={user} label="id">{user.id}</:col>
+ <:col :let={user} label="username">{user.username}</:col>
+ </.table>
+ """
+ attr :id, :string, required: true
+ attr :rows, :list, required: true
+ attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
+
+ attr :row_item, :any,
+ default: &Function.identity/1,
+ doc: "the function for mapping each row before calling the :col and :action slots"
+
+ slot :col, required: true do
+ attr :label, :string
+ end
+
+ slot :action, doc: "the slot for showing user actions in the last table column"
+
+ def table(assigns) do
+ assigns =
+ with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
+ assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
+ end
+
+ ~H"""
+ <table class="table table-zebra">
+ <thead>
+ <tr>
+ <th :for={col <- @col}>{col[:label]}</th>
+ <th :if={@action != []}>
+ <span class="sr-only">{gettext("Actions")}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
+ <tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
+ <td
+ :for={col <- @col}
+ phx-click={@row_click && @row_click.(row)}
+ class={@row_click && "hover:cursor-pointer"}
+ >
+ {render_slot(col, @row_item.(row))}
+ </td>
+ <td :if={@action != []} class="w-0 font-semibold">
+ <div class="flex gap-4">
+ <%= for action <- @action do %>
+ {render_slot(action, @row_item.(row))}
+ <% end %>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ """
+ end
+
+ @doc """
+ Renders a data list.
+
+ ## Examples
+
+ <.list>
+ <:item title="Title">{@post.title}</:item>
+ <:item title="Views">{@post.views}</:item>
+ </.list>
+ """
+ slot :item, required: true do
+ attr :title, :string, required: true
+ end
+
+ def list(assigns) do
+ ~H"""
+ <ul class="list">
+ <li :for={item <- @item} class="list-row">
+ <div class="list-col-grow">
+ <div class="font-bold">{item.title}</div>
+ <div>{render_slot(item)}</div>
+ </div>
+ </li>
+ </ul>
+ """
+ end
+
+ @doc """
+ Renders a [Heroicon](https://heroicons.com).
+
+ Heroicons come in three styles – outline, solid, and mini.
+ By default, the outline style is used, but solid and mini may
+ be applied by using the `-solid` and `-mini` suffix.
+
+ You can customize the size and colors of the icons by setting
+ width, height, and background color classes.
+
+ Icons are extracted from the `deps/heroicons` directory and bundled within
+ your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
+
+ ## Examples
+
+ <.icon name="hero-x-mark" />
+ <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
+ """
+ attr :name, :string, required: true
+ attr :class, :any, default: "size-4"
+
+ def icon(%{name: "hero-" <> _} = assigns) do
+ ~H"""
+ <span class={[@name, @class]} />
+ """
+ end
+
+ ## JS Commands
+
+ def show(js \\ %JS{}, selector) do
+ JS.show(js,
+ to: selector,
+ time: 300,
+ transition:
+ {"transition-all ease-out duration-300",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+ "opacity-100 translate-y-0 sm:scale-100"}
+ )
+ end
+
+ def hide(js \\ %JS{}, selector) do
+ JS.hide(js,
+ to: selector,
+ time: 200,
+ transition:
+ {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+ )
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # When using gettext, we typically pass the strings we want
+ # to translate as a static argument:
+ #
+ # # Translate the number of files with plural rules
+ # dngettext("errors", "1 file", "%{count} files", count)
+ #
+ # However the error messages in our forms and APIs are generated
+ # dynamically, so we need to translate them by calling Gettext
+ # with our gettext backend as first argument. Translations are
+ # available in the errors.po file (as we use the "errors" domain).
+ if count = opts[:count] do
+ Gettext.dngettext(DailyTrackerWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(DailyTrackerWeb.Gettext, "errors", msg, opts)
+ end
+ end
+
+ @doc """
+ Translates the errors for a field from a keyword list of errors.
+ """
+ def translate_errors(errors, field) when is_list(errors) do
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+ end
+end
diff --git a/lib/daily_tracker_web/components/layouts.ex b/lib/daily_tracker_web/components/layouts.ex
@@ -0,0 +1,154 @@
+defmodule DailyTrackerWeb.Layouts do
+ @moduledoc """
+ This module holds layouts and related functionality
+ used by your application.
+ """
+ use DailyTrackerWeb, :html
+
+ # Embed all files in layouts/* within this module.
+ # The default root.html.heex file contains the HTML
+ # skeleton of your application, namely HTML headers
+ # and other static content.
+ embed_templates "layouts/*"
+
+ @doc """
+ Renders your app layout.
+
+ This function is typically invoked from every template,
+ and it often contains your application menu, sidebar,
+ or similar.
+
+ ## Examples
+
+ <Layouts.app flash={@flash}>
+ <h1>Content</h1>
+ </Layouts.app>
+
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+
+ attr :current_scope, :map,
+ default: nil,
+ doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
+
+ slot :inner_block, required: true
+
+ def app(assigns) do
+ ~H"""
+ <header class="navbar px-4 sm:px-6 lg:px-8">
+ <div class="flex-1">
+ <a href="/" class="flex-1 flex w-fit items-center gap-2">
+ <img src={~p"/images/logo.svg"} width="36" />
+ <span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
+ </a>
+ </div>
+ <div class="flex-none">
+ <ul class="flex flex-column px-1 space-x-4 items-center">
+ <li>
+ <a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
+ </li>
+ <li>
+ <a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
+ </li>
+ <li>
+ <.theme_toggle />
+ </li>
+ <li>
+ <a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
+ Get Started <span aria-hidden="true">→</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </header>
+
+ <main class="px-4 py-20 sm:px-6 lg:px-8">
+ <div class="mx-auto max-w-2xl space-y-4">
+ {render_slot(@inner_block)}
+ </div>
+ </main>
+
+ <.flash_group flash={@flash} />
+ """
+ end
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+ attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
+
+ def flash_group(assigns) do
+ ~H"""
+ <div id={@id} aria-live="polite">
+ <.flash kind={:info} flash={@flash} />
+ <.flash kind={:error} flash={@flash} />
+
+ <.flash
+ id="client-error"
+ kind={:error}
+ title={gettext("We can't find the internet")}
+ phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
+ phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
+ hidden
+ >
+ {gettext("Attempting to reconnect")}
+ <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
+ </.flash>
+
+ <.flash
+ id="server-error"
+ kind={:error}
+ title={gettext("Something went wrong!")}
+ phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
+ phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
+ hidden
+ >
+ {gettext("Attempting to reconnect")}
+ <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
+ </.flash>
+ </div>
+ """
+ end
+
+ @doc """
+ Provides dark vs light theme toggle based on themes defined in app.css.
+
+ See <head> in root.html.heex which applies the theme before page load.
+ """
+ def theme_toggle(assigns) do
+ ~H"""
+ <div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
+ <div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 transition-[left]" />
+
+ <button
+ class="flex p-2 cursor-pointer w-1/3"
+ phx-click={JS.dispatch("phx:set-theme")}
+ data-phx-theme="system"
+ >
+ <.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
+ </button>
+
+ <button
+ class="flex p-2 cursor-pointer w-1/3"
+ phx-click={JS.dispatch("phx:set-theme")}
+ data-phx-theme="light"
+ >
+ <.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
+ </button>
+
+ <button
+ class="flex p-2 cursor-pointer w-1/3"
+ phx-click={JS.dispatch("phx:set-theme")}
+ data-phx-theme="dark"
+ >
+ <.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
+ </button>
+ </div>
+ """
+ end
+end
diff --git a/lib/daily_tracker_web/components/layouts/root.html.heex b/lib/daily_tracker_web/components/layouts/root.html.heex
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="csrf-token" content={get_csrf_token()} />
+ <.live_title default="DailyTracker" suffix=" · Phoenix Framework" phx-no-format>{assigns[:page_title]}</.live_title>
+ <link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
+ <script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
+ </script>
+ <script>
+ (() => {
+ const setTheme = (theme) => {
+ if (theme === "system") {
+ localStorage.removeItem("phx:theme");
+ document.documentElement.removeAttribute("data-theme");
+ } else {
+ localStorage.setItem("phx:theme", theme);
+ document.documentElement.setAttribute("data-theme", theme);
+ }
+ };
+ if (!document.documentElement.hasAttribute("data-theme")) {
+ setTheme(localStorage.getItem("phx:theme") || "system");
+ }
+ window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
+
+ window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
+ })();
+ </script>
+ </head>
+ <body>
+ <ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
+ <%= if @current_scope do %>
+ <li>
+ {@current_scope.user.email}
+ </li>
+ <li>
+ <.link href={~p"/users/settings"}>Settings</.link>
+ </li>
+ <li>
+ <.link href={~p"/users/log-out"} method="delete">Log out</.link>
+ </li>
+ <% else %>
+ <li>
+ <.link href={~p"/users/register"}>Register</.link>
+ </li>
+ <li>
+ <.link href={~p"/users/log-in"}>Log in</.link>
+ </li>
+ <% end %>
+ </ul>
+ {@inner_content}
+ </body>
+</html>
diff --git a/lib/daily_tracker_web/controllers/error_html.ex b/lib/daily_tracker_web/controllers/error_html.ex
@@ -0,0 +1,24 @@
+defmodule DailyTrackerWeb.ErrorHTML do
+ @moduledoc """
+ This module is invoked by your endpoint in case of errors on HTML requests.
+
+ See config/config.exs.
+ """
+ use DailyTrackerWeb, :html
+
+ # If you want to customize your error pages,
+ # uncomment the embed_templates/1 call below
+ # and add pages to the error directory:
+ #
+ # * lib/daily_tracker_web/controllers/error_html/404.html.heex
+ # * lib/daily_tracker_web/controllers/error_html/500.html.heex
+ #
+ # embed_templates "error_html/*"
+
+ # The default is to render a plain text page based on
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/lib/daily_tracker_web/controllers/error_json.ex b/lib/daily_tracker_web/controllers/error_json.ex
@@ -0,0 +1,21 @@
+defmodule DailyTrackerWeb.ErrorJSON do
+ @moduledoc """
+ This module is invoked by your endpoint in case of errors on JSON requests.
+
+ See config/config.exs.
+ """
+
+ # If you want to customize a particular status code,
+ # you may add your own clauses, such as:
+ #
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/lib/daily_tracker_web/controllers/page_controller.ex b/lib/daily_tracker_web/controllers/page_controller.ex
@@ -0,0 +1,7 @@
+defmodule DailyTrackerWeb.PageController do
+ use DailyTrackerWeb, :controller
+
+ def home(conn, _params) do
+ render(conn, :home)
+ end
+end
diff --git a/lib/daily_tracker_web/controllers/page_html.ex b/lib/daily_tracker_web/controllers/page_html.ex
@@ -0,0 +1,10 @@
+defmodule DailyTrackerWeb.PageHTML do
+ @moduledoc """
+ This module contains pages rendered by PageController.
+
+ See the `page_html` directory for all templates available.
+ """
+ use DailyTrackerWeb, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/lib/daily_tracker_web/controllers/page_html/home.html.heex b/lib/daily_tracker_web/controllers/page_html/home.html.heex
@@ -0,0 +1,202 @@
+<Layouts.flash_group flash={@flash} />
+<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
+ <svg
+ viewBox="0 0 1480 957"
+ fill="none"
+ aria-hidden="true"
+ class="absolute inset-0 h-full w-full"
+ preserveAspectRatio="xMinYMid slice"
+ >
+ <path fill="#EE7868" d="M0 0h1480v957H0z" />
+ <path
+ d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
+ fill="#FF9F92"
+ />
+ <path
+ d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
+ fill="#FA8372"
+ />
+ <path
+ d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
+ fill="#E96856"
+ fill-opacity=".6"
+ />
+ <path
+ d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
+ fill="#C42652"
+ fill-opacity=".2"
+ />
+ <path
+ d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
+ fill="#A41C42"
+ fill-opacity=".2"
+ />
+ <path
+ d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
+ fill="#A41C42"
+ fill-opacity=".2"
+ />
+ </svg>
+</div>
+<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
+ <div class="mx-auto max-w-xl lg:mx-0">
+ <svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
+ <path
+ d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
+ fill="#FD4F00"
+ />
+ </svg>
+ <div class="mt-10 flex justify-between items-center">
+ <h1 class="flex items-center text-sm font-semibold leading-6">
+ Phoenix Framework
+ <small class="badge badge-warning badge-sm ml-3">
+ v{Application.spec(:phoenix, :vsn)}
+ </small>
+ </h1>
+ <Layouts.theme_toggle />
+ </div>
+
+ <p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-balance">
+ Peace of mind from prototype to production.
+ </p>
+ <p class="mt-4 leading-7 text-base-content/70">
+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
+ </p>
+ <div class="flex">
+ <div class="w-full sm:w-auto">
+ <div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
+ <a
+ href="https://hexdocs.pm/phoenix/overview.html"
+ class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
+ >
+ <span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
+ </span>
+ <span class="relative flex items-center gap-4 sm:flex-col">
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
+ <path d="m12 4 10-2v18l-10 2V4Z" fill="currentColor" fill-opacity=".15" />
+ <path
+ d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ Guides & Docs
+ </span>
+ </a>
+ <a
+ href="https://github.com/phoenixframework/phoenix"
+ class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
+ >
+ <span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
+ </span>
+ <span class="relative flex items-center gap-4 sm:flex-col">
+ <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
+ <path
+ fill="currentColor"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
+ />
+ </svg>
+ Source Code
+ </span>
+ </a>
+ <a
+ href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
+ class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
+ >
+ <span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
+ </span>
+ <span class="relative flex items-center gap-4 sm:flex-col">
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
+ <path
+ d="M12 1v6M12 17v6"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <circle
+ cx="12"
+ cy="12"
+ r="4"
+ fill="currentColor"
+ fill-opacity=".15"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ Changelog
+ </span>
+ </a>
+ </div>
+ <div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-base-content/80 sm:grid-cols-2">
+ <div>
+ <a
+ href="https://elixirforum.com"
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
+ >
+ <svg
+ viewBox="0 0 16 16"
+ aria-hidden="true"
+ class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
+ >
+ <path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
+ </svg>
+ Discuss on the Elixir Forum
+ </a>
+ </div>
+ <div>
+ <a
+ href="https://discord.gg/elixir"
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
+ >
+ <svg
+ viewBox="0 0 16 16"
+ aria-hidden="true"
+ class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
+ >
+ <path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
+ </svg>
+ Join our Discord server
+ </a>
+ </div>
+ <div>
+ <a
+ href="https://elixir-slack.community/"
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
+ >
+ <svg
+ viewBox="0 0 16 16"
+ aria-hidden="true"
+ class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
+ >
+ <path d="M3.361 10.11a1.68 1.68 0 1 1-1.68-1.681h1.68v1.682ZM4.209 10.11a1.68 1.68 0 1 1 3.361 0v4.21a1.68 1.68 0 1 1-3.361 0v-4.21ZM5.89 3.361a1.68 1.68 0 1 1 1.681-1.68v1.68H5.89ZM5.89 4.209a1.68 1.68 0 1 1 0 3.361H1.68a1.68 1.68 0 1 1 0-3.361h4.21ZM12.639 5.89a1.68 1.68 0 1 1 1.68 1.681h-1.68V5.89ZM11.791 5.89a1.68 1.68 0 1 1-3.361 0V1.68a1.68 1.68 0 0 1 3.361 0v4.21ZM10.11 12.639a1.68 1.68 0 1 1-1.681 1.68v-1.68h1.682ZM10.11 11.791a1.68 1.68 0 1 1 0-3.361h4.21a1.68 1.68 0 1 1 0 3.361h-4.21Z" />
+ </svg>
+ Join us on Slack
+ </a>
+ </div>
+ <div>
+ <a
+ href="https://fly.io/docs/elixir/getting-started/"
+ class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
+ >
+ <svg
+ viewBox="0 0 20 20"
+ aria-hidden="true"
+ class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
+ >
+ <path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
+ </svg>
+ Deploy your application
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/lib/daily_tracker_web/controllers/user_session_controller.ex b/lib/daily_tracker_web/controllers/user_session_controller.ex
@@ -0,0 +1,67 @@
+defmodule DailyTrackerWeb.UserSessionController do
+ use DailyTrackerWeb, :controller
+
+ alias DailyTracker.Accounts
+ alias DailyTrackerWeb.UserAuth
+
+ def create(conn, %{"_action" => "confirmed"} = params) do
+ create(conn, params, "User confirmed successfully.")
+ end
+
+ def create(conn, params) do
+ create(conn, params, "Welcome back!")
+ end
+
+ # magic link login
+ defp create(conn, %{"user" => %{"token" => token} = user_params}, info) do
+ case Accounts.login_user_by_magic_link(token) do
+ {:ok, {user, tokens_to_disconnect}} ->
+ UserAuth.disconnect_sessions(tokens_to_disconnect)
+
+ conn
+ |> put_flash(:info, info)
+ |> UserAuth.log_in_user(user, user_params)
+
+ _ ->
+ conn
+ |> put_flash(:error, "The link is invalid or it has expired.")
+ |> redirect(to: ~p"/users/log-in")
+ end
+ end
+
+ # email + password login
+ defp create(conn, %{"user" => user_params}, info) do
+ %{"email" => email, "password" => password} = user_params
+
+ if user = Accounts.get_user_by_email_and_password(email, password) do
+ conn
+ |> put_flash(:info, info)
+ |> UserAuth.log_in_user(user, user_params)
+ else
+ # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
+ conn
+ |> put_flash(:error, "Invalid email or password")
+ |> put_flash(:email, String.slice(email, 0, 160))
+ |> redirect(to: ~p"/users/log-in")
+ end
+ end
+
+ def update_password(conn, %{"user" => user_params} = params) do
+ user = conn.assigns.current_scope.user
+ true = Accounts.sudo_mode?(user)
+ {:ok, {_user, expired_tokens}} = Accounts.update_user_password(user, user_params)
+
+ # disconnect all existing LiveViews with old sessions
+ UserAuth.disconnect_sessions(expired_tokens)
+
+ conn
+ |> put_session(:user_return_to, ~p"/users/settings")
+ |> create(params, "Password updated successfully!")
+ end
+
+ def delete(conn, _params) do
+ conn
+ |> put_flash(:info, "Logged out successfully.")
+ |> UserAuth.log_out_user()
+ end
+end
diff --git a/lib/daily_tracker_web/endpoint.ex b/lib/daily_tracker_web/endpoint.ex
@@ -0,0 +1,55 @@
+defmodule DailyTrackerWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :daily_tracker
+
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_daily_tracker_key",
+ signing_salt: "tg+Ej1Xb",
+ same_site: "Lax"
+ ]
+
+ socket "/live", Phoenix.LiveView.Socket,
+ websocket: [connect_info: [session: @session_options]],
+ longpoll: [connect_info: [session: @session_options]]
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # When code reloading is disabled (e.g., in production),
+ # the `gzip` option is enabled to serve compressed
+ # static files generated by running `phx.digest`.
+ plug Plug.Static,
+ at: "/",
+ from: :daily_tracker,
+ gzip: not code_reloading?,
+ only: DailyTrackerWeb.static_paths(),
+ raise_on_missing_only: code_reloading?
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :daily_tracker
+ end
+
+ plug Phoenix.LiveDashboard.RequestLogger,
+ param_key: "request_logger",
+ cookie_key: "request_logger"
+
+ plug Plug.RequestId
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+ plug Plug.Session, @session_options
+ plug DailyTrackerWeb.Router
+end
diff --git a/lib/daily_tracker_web/gettext.ex b/lib/daily_tracker_web/gettext.ex
@@ -0,0 +1,25 @@
+defmodule DailyTrackerWeb.Gettext do
+ @moduledoc """
+ A module providing Internationalization with a gettext-based API.
+
+ By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations
+ that you can use in your application. To use this Gettext backend module,
+ call `use Gettext` and pass it as an option:
+
+ use Gettext, backend: DailyTrackerWeb.Gettext
+
+ # Simple translation
+ gettext("Here is the string to translate")
+
+ # Plural translation
+ ngettext("Here is the string to translate",
+ "Here are the strings to translate",
+ 3)
+
+ # Domain-based translation
+ dgettext("errors", "Here is the error message to translate")
+
+ See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
+ """
+ use Gettext.Backend, otp_app: :daily_tracker
+end
diff --git a/lib/daily_tracker_web/live/entry_live/form.ex b/lib/daily_tracker_web/live/entry_live/form.ex
@@ -0,0 +1,102 @@
+defmodule DailyTrackerWeb.EntryLive.Form do
+ use DailyTrackerWeb, :live_view
+
+ alias DailyTracker.Tracker
+ alias DailyTracker.Tracker.Entry
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash}>
+ <.header>
+ {@page_title}
+ <:subtitle>Use this form to manage entry records in your database.</:subtitle>
+ </.header>
+
+ <.form for={@form} id="entry-form" phx-change="validate" phx-submit="save">
+ <.input field={@form[:date]} type="date" label="Date" />
+ <.input field={@form[:mood]} type="text" label="Mood" />
+ <.input field={@form[:headache]} type="checkbox" label="Headache" />
+ <.input field={@form[:water_glasses]} type="number" label="Water glasses" />
+ <.input field={@form[:worked_out]} type="checkbox" label="Worked out" />
+ <.input field={@form[:bowel_movement]} type="checkbox" label="Bowel movement" />
+ <.input field={@form[:hunger_level]} type="number" label="Hunger level" />
+ <.input field={@form[:period]} type="checkbox" label="Period" />
+ <.input field={@form[:notes]} type="textarea" label="Notes" />
+ <footer>
+ <.button phx-disable-with="Saving..." variant="primary">Save Entry</.button>
+ <.button navigate={return_path(@return_to, @entry)}>Cancel</.button>
+ </footer>
+ </.form>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:return_to, return_to(params["return_to"]))
+ |> apply_action(socket.assigns.live_action, params)}
+ end
+
+ defp return_to("show"), do: "show"
+ defp return_to(_), do: "index"
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ entry = Tracker.get_entry!(socket.assigns.current_scope, id)
+
+ socket
+ |> assign(:page_title, "Edit Entry")
+ |> assign(:entry, entry)
+ |> assign(:form, to_form(Tracker.change_entry(entry)))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ entry = %Entry{}
+
+ socket
+ |> assign(:page_title, "New Entry")
+ |> assign(:entry, entry)
+ |> assign(:form, to_form(Tracker.change_entry(entry)))
+ end
+
+ @impl true
+ def handle_event("validate", %{"entry" => entry_params}, socket) do
+ changeset = Tracker.change_entry(socket.assigns.entry, entry_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"entry" => entry_params}, socket) do
+ save_entry(socket, socket.assigns.live_action, entry_params)
+ end
+
+ defp save_entry(socket, :edit, entry_params) do
+ case Tracker.update_entry(socket.assigns.entry, entry_params) do
+ {:ok, entry} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Entry updated successfully")
+ |> push_navigate(to: return_path(socket.assigns.return_to, entry))}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_entry(socket, :new, entry_params) do
+ case Tracker.create_entry(socket.assigns.current_scope, entry_params) do
+ {:ok, entry} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Entry created successfully")
+ |> push_navigate(to: return_path(socket.assigns.return_to, entry))}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp return_path("index", _entry), do: ~p"/entries"
+ defp return_path("show", entry), do: ~p"/entries/#{entry}"
+end
diff --git a/lib/daily_tracker_web/live/entry_live/index.ex b/lib/daily_tracker_web/live/entry_live/index.ex
@@ -0,0 +1,67 @@
+defmodule DailyTrackerWeb.EntryLive.Index do
+ use DailyTrackerWeb, :live_view
+
+ alias DailyTracker.Tracker
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash}>
+ <.header>
+ Listing Entries
+ <:actions>
+ <.button variant="primary" navigate={~p"/entries/new"}>
+ <.icon name="hero-plus" /> New Entry
+ </.button>
+ </:actions>
+ </.header>
+
+ <.table
+ id="entries"
+ rows={@streams.entries}
+ row_click={fn {_id, entry} -> JS.navigate(~p"/entries/#{entry}") end}
+ >
+ <:col :let={{_id, entry}} label="Date">{entry.date}</:col>
+ <:col :let={{_id, entry}} label="Mood">{entry.mood}</:col>
+ <:col :let={{_id, entry}} label="Headache">{entry.headache}</:col>
+ <:col :let={{_id, entry}} label="Water glasses">{entry.water_glasses}</:col>
+ <:col :let={{_id, entry}} label="Worked out">{entry.worked_out}</:col>
+ <:col :let={{_id, entry}} label="Bowel movement">{entry.bowel_movement}</:col>
+ <:col :let={{_id, entry}} label="Hunger level">{entry.hunger_level}</:col>
+ <:col :let={{_id, entry}} label="Period">{entry.period}</:col>
+ <:col :let={{_id, entry}} label="Notes">{entry.notes}</:col>
+ <:action :let={{_id, entry}}>
+ <div class="sr-only">
+ <.link navigate={~p"/entries/#{entry}"}>Show</.link>
+ </div>
+ <.link navigate={~p"/entries/#{entry}/edit"}>Edit</.link>
+ </:action>
+ <:action :let={{id, entry}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: entry.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+ </.link>
+ </:action>
+ </.table>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:page_title, "Listing Entries")
+ |> stream(:entries, Tracker.list_entries(socket.assigns.current_scope))}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ entry = Tracker.get_entry!(socket.assigns.current_scope, id)
+ {:ok, _} = Tracker.delete_entry(entry)
+
+ {:noreply, stream_delete(socket, :entries, entry)}
+ end
+end
diff --git a/lib/daily_tracker_web/live/entry_live/show.ex b/lib/daily_tracker_web/live/entry_live/show.ex
@@ -0,0 +1,45 @@
+defmodule DailyTrackerWeb.EntryLive.Show do
+ use DailyTrackerWeb, :live_view
+
+ alias DailyTracker.Tracker
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash}>
+ <.header>
+ Entry {@entry.id}
+ <:subtitle>This is a entry record from your database.</:subtitle>
+ <:actions>
+ <.button navigate={~p"/entries"}>
+ <.icon name="hero-arrow-left" />
+ </.button>
+ <.button variant="primary" navigate={~p"/entries/#{@entry}/edit?return_to=show"}>
+ <.icon name="hero-pencil-square" /> Edit entry
+ </.button>
+ </:actions>
+ </.header>
+
+ <.list>
+ <:item title="Date">{@entry.date}</:item>
+ <:item title="Mood">{@entry.mood}</:item>
+ <:item title="Headache">{@entry.headache}</:item>
+ <:item title="Water glasses">{@entry.water_glasses}</:item>
+ <:item title="Worked out">{@entry.worked_out}</:item>
+ <:item title="Bowel movement">{@entry.bowel_movement}</:item>
+ <:item title="Hunger level">{@entry.hunger_level}</:item>
+ <:item title="Period">{@entry.period}</:item>
+ <:item title="Notes">{@entry.notes}</:item>
+ </.list>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(%{"id" => id}, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:page_title, "Show Entry")
+ |> assign(:entry, Tracker.get_entry!(socket.assigns.current_scope, id))}
+ end
+end
diff --git a/lib/daily_tracker_web/live/user_live/confirmation.ex b/lib/daily_tracker_web/live/user_live/confirmation.ex
@@ -0,0 +1,94 @@
+defmodule DailyTrackerWeb.UserLive.Confirmation do
+ use DailyTrackerWeb, :live_view
+
+ alias DailyTracker.Accounts
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash} current_scope={@current_scope}>
+ <div class="mx-auto max-w-sm">
+ <div class="text-center">
+ <.header>Welcome {@user.email}</.header>
+ </div>
+
+ <.form
+ :if={!@user.confirmed_at}
+ for={@form}
+ id="confirmation_form"
+ phx-mounted={JS.focus_first()}
+ phx-submit="submit"
+ action={~p"/users/log-in?_action=confirmed"}
+ phx-trigger-action={@trigger_submit}
+ >
+ <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+ <.button
+ name={@form[:remember_me].name}
+ value="true"
+ phx-disable-with="Confirming..."
+ class="btn btn-primary w-full"
+ >
+ Confirm and stay logged in
+ </.button>
+ <.button phx-disable-with="Confirming..." class="btn btn-primary btn-soft w-full mt-2">
+ Confirm and log in only this time
+ </.button>
+ </.form>
+
+ <.form
+ :if={@user.confirmed_at}
+ for={@form}
+ id="login_form"
+ phx-submit="submit"
+ phx-mounted={JS.focus_first()}
+ action={~p"/users/log-in"}
+ phx-trigger-action={@trigger_submit}
+ >
+ <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+ <%= if @current_scope do %>
+ <.button phx-disable-with="Logging in..." class="btn btn-primary w-full">
+ Log in
+ </.button>
+ <% else %>
+ <.button
+ name={@form[:remember_me].name}
+ value="true"
+ phx-disable-with="Logging in..."
+ class="btn btn-primary w-full"
+ >
+ Keep me logged in on this device
+ </.button>
+ <.button phx-disable-with="Logging in..." class="btn btn-primary btn-soft w-full mt-2">
+ Log me in only this time
+ </.button>
+ <% end %>
+ </.form>
+
+ <p :if={!@user.confirmed_at} class="alert alert-outline mt-8">
+ Tip: If you prefer passwords, you can enable them in the user settings.
+ </p>
+ </div>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(%{"token" => token}, _session, socket) do
+ if user = Accounts.get_user_by_magic_link_token(token) do
+ form = to_form(%{"token" => token}, as: "user")
+
+ {:ok, assign(socket, user: user, form: form, trigger_submit: false),
+ temporary_assigns: [form: nil]}
+ else
+ {:ok,
+ socket
+ |> put_flash(:error, "Magic link is invalid or it has expired.")
+ |> push_navigate(to: ~p"/users/log-in")}
+ end
+ end
+
+ @impl true
+ def handle_event("submit", %{"user" => params}, socket) do
+ {:noreply, assign(socket, form: to_form(params, as: "user"), trigger_submit: true)}
+ end
+end
diff --git a/lib/daily_tracker_web/live/user_live/login.ex b/lib/daily_tracker_web/live/user_live/login.ex
@@ -0,0 +1,134 @@
+defmodule DailyTrackerWeb.UserLive.Login do
+ use DailyTrackerWeb, :live_view
+
+ alias DailyTracker.Accounts
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash} current_scope={@current_scope}>
+ <div class="mx-auto max-w-sm space-y-4">
+ <div class="text-center">
+ <.header>
+ <p>Log in</p>
+ <:subtitle>
+ <%= if @current_scope do %>
+ You need to reauthenticate to perform sensitive actions on your account.
+ <% else %>
+ Don't have an account? <.link
+ navigate={~p"/users/register"}
+ class="font-semibold text-brand hover:underline"
+ phx-no-format
+ >Sign up</.link> for an account now.
+ <% end %>
+ </:subtitle>
+ </.header>
+ </div>
+
+ <div :if={local_mail_adapter?()} class="alert alert-info">
+ <.icon name="hero-information-circle" class="size-6 shrink-0" />
+ <div>
+ <p>You are running the local mail adapter.</p>
+ <p>
+ To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+ </p>
+ </div>
+ </div>
+
+ <.form
+ :let={f}
+ for={@form}
+ id="login_form_magic"
+ action={~p"/users/log-in"}
+ phx-submit="submit_magic"
+ >
+ <.input
+ readonly={!!@current_scope}
+ field={f[:email]}
+ type="email"
+ label="Email"
+ autocomplete="username"
+ spellcheck="false"
+ required
+ phx-mounted={JS.focus()}
+ />
+ <.button class="btn btn-primary w-full">
+ Log in with email <span aria-hidden="true">→</span>
+ </.button>
+ </.form>
+
+ <div class="divider">or</div>
+
+ <.form
+ :let={f}
+ for={@form}
+ id="login_form_password"
+ action={~p"/users/log-in"}
+ phx-submit="submit_password"
+ phx-trigger-action={@trigger_submit}
+ >
+ <.input
+ readonly={!!@current_scope}
+ field={f[:email]}
+ type="email"
+ label="Email"
+ autocomplete="username"
+ spellcheck="false"
+ required
+ />
+ <.input
+ field={@form[:password]}
+ type="password"
+ label="Password"
+ autocomplete="current-password"
+ spellcheck="false"
+ />
+ <.button class="btn btn-primary w-full" name={@form[:remember_me].name} value="true">
+ Log in and stay logged in <span aria-hidden="true">→</span>
+ </.button>
+ <.button class="btn btn-primary btn-soft w-full mt-2">
+ Log in only this time
+ </.button>
+ </.form>
+ </div>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(_params, _session, socket) do
+ email =
+ Phoenix.Flash.get(socket.assigns.flash, :email) ||
+ get_in(socket.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
+
+ form = to_form(%{"email" => email}, as: "user")
+
+ {:ok, assign(socket, form: form, trigger_submit: false)}
+ end
+
+ @impl true
+ def handle_event("submit_password", _params, socket) do
+ {:noreply, assign(socket, :trigger_submit, true)}
+ end
+
+ def handle_event("submit_magic", %{"user" => %{"email" => email}}, socket) do
+ if user = Accounts.get_user_by_email(email) do
+ Accounts.deliver_login_instructions(
+ user,
+ &url(~p"/users/log-in/#{&1}")
+ )
+ end
+
+ info =
+ "If your email is in our system, you will receive instructions for logging in shortly."
+
+ {:noreply,
+ socket
+ |> put_flash(:info, info)
+ |> push_navigate(to: ~p"/users/log-in")}
+ end
+
+ defp local_mail_adapter? do
+ Application.get_env(:daily_tracker, DailyTracker.Mailer)[:adapter] == Swoosh.Adapters.Local
+ end
+end
diff --git a/lib/daily_tracker_web/live/user_live/registration.ex b/lib/daily_tracker_web/live/user_live/registration.ex
@@ -0,0 +1,89 @@
+defmodule DailyTrackerWeb.UserLive.Registration do
+ use DailyTrackerWeb, :live_view
+
+ alias DailyTracker.Accounts
+ alias DailyTracker.Accounts.User
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash} current_scope={@current_scope}>
+ <div class="mx-auto max-w-sm">
+ <div class="text-center">
+ <.header>
+ Register for an account
+ <:subtitle>
+ Already registered?
+ <.link navigate={~p"/users/log-in"} class="font-semibold text-brand hover:underline">
+ Log in
+ </.link>
+ to your account now.
+ </:subtitle>
+ </.header>
+ </div>
+
+ <.form for={@form} id="registration_form" phx-submit="save" phx-change="validate">
+ <.input
+ field={@form[:email]}
+ type="email"
+ label="Email"
+ autocomplete="username"
+ spellcheck="false"
+ required
+ phx-mounted={JS.focus()}
+ />
+
+ <.button phx-disable-with="Creating account..." class="btn btn-primary w-full">
+ Create an account
+ </.button>
+ </.form>
+ </div>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(_params, _session, %{assigns: %{current_scope: %{user: user}}} = socket)
+ when not is_nil(user) do
+ {:ok, redirect(socket, to: DailyTrackerWeb.UserAuth.signed_in_path(socket))}
+ end
+
+ def mount(_params, _session, socket) do
+ changeset = Accounts.change_user_email(%User{}, %{}, validate_unique: false)
+
+ {:ok, assign_form(socket, changeset), temporary_assigns: [form: nil]}
+ end
+
+ @impl true
+ def handle_event("save", %{"user" => user_params}, socket) do
+ case Accounts.register_user(user_params) do
+ {:ok, user} ->
+ {:ok, _} =
+ Accounts.deliver_login_instructions(
+ user,
+ &url(~p"/users/log-in/#{&1}")
+ )
+
+ {:noreply,
+ socket
+ |> put_flash(
+ :info,
+ "An email was sent to #{user.email}, please access it to confirm your account."
+ )
+ |> push_navigate(to: ~p"/users/log-in")}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign_form(socket, changeset)}
+ end
+ end
+
+ def handle_event("validate", %{"user" => user_params}, socket) do
+ changeset = Accounts.change_user_email(%User{}, user_params, validate_unique: false)
+ {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
+ end
+
+ defp assign_form(socket, %Ecto.Changeset{} = changeset) do
+ form = to_form(changeset, as: "user")
+ assign(socket, form: form)
+ end
+end
diff --git a/lib/daily_tracker_web/live/user_live/settings.ex b/lib/daily_tracker_web/live/user_live/settings.ex
@@ -0,0 +1,160 @@
+defmodule DailyTrackerWeb.UserLive.Settings do
+ use DailyTrackerWeb, :live_view
+
+ on_mount {DailyTrackerWeb.UserAuth, :require_sudo_mode}
+
+ alias DailyTracker.Accounts
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <Layouts.app flash={@flash} current_scope={@current_scope}>
+ <div class="text-center">
+ <.header>
+ Account Settings
+ <:subtitle>Manage your account email address and password settings</:subtitle>
+ </.header>
+ </div>
+
+ <.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email">
+ <.input
+ field={@email_form[:email]}
+ type="email"
+ label="Email"
+ autocomplete="username"
+ spellcheck="false"
+ required
+ />
+ <.button variant="primary" phx-disable-with="Changing...">Change Email</.button>
+ </.form>
+
+ <div class="divider" />
+
+ <.form
+ for={@password_form}
+ id="password_form"
+ action={~p"/users/update-password"}
+ method="post"
+ phx-change="validate_password"
+ phx-submit="update_password"
+ phx-trigger-action={@trigger_submit}
+ >
+ <input
+ name={@password_form[:email].name}
+ type="hidden"
+ id="hidden_user_email"
+ spellcheck="false"
+ value={@current_email}
+ />
+ <.input
+ field={@password_form[:password]}
+ type="password"
+ label="New password"
+ autocomplete="new-password"
+ spellcheck="false"
+ required
+ />
+ <.input
+ field={@password_form[:password_confirmation]}
+ type="password"
+ label="Confirm new password"
+ autocomplete="new-password"
+ spellcheck="false"
+ />
+ <.button variant="primary" phx-disable-with="Saving...">
+ Save Password
+ </.button>
+ </.form>
+ </Layouts.app>
+ """
+ end
+
+ @impl true
+ def mount(%{"token" => token}, _session, socket) do
+ socket =
+ case Accounts.update_user_email(socket.assigns.current_scope.user, token) do
+ {:ok, _user} ->
+ put_flash(socket, :info, "Email changed successfully.")
+
+ {:error, _} ->
+ put_flash(socket, :error, "Email change link is invalid or it has expired.")
+ end
+
+ {:ok, push_navigate(socket, to: ~p"/users/settings")}
+ end
+
+ def mount(_params, _session, socket) do
+ user = socket.assigns.current_scope.user
+ email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false)
+ password_changeset = Accounts.change_user_password(user, %{}, hash_password: false)
+
+ socket =
+ socket
+ |> assign(:current_email, user.email)
+ |> assign(:email_form, to_form(email_changeset))
+ |> assign(:password_form, to_form(password_changeset))
+ |> assign(:trigger_submit, false)
+
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_event("validate_email", params, socket) do
+ %{"user" => user_params} = params
+
+ email_form =
+ socket.assigns.current_scope.user
+ |> Accounts.change_user_email(user_params, validate_unique: false)
+ |> Map.put(:action, :validate)
+ |> to_form()
+
+ {:noreply, assign(socket, email_form: email_form)}
+ end
+
+ def handle_event("update_email", params, socket) do
+ %{"user" => user_params} = params
+ user = socket.assigns.current_scope.user
+ true = Accounts.sudo_mode?(user)
+
+ case Accounts.change_user_email(user, user_params) do
+ %{valid?: true} = changeset ->
+ Accounts.deliver_user_update_email_instructions(
+ Ecto.Changeset.apply_action!(changeset, :insert),
+ user.email,
+ &url(~p"/users/settings/confirm-email/#{&1}")
+ )
+
+ info = "A link to confirm your email change has been sent to the new address."
+ {:noreply, socket |> put_flash(:info, info)}
+
+ changeset ->
+ {:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))}
+ end
+ end
+
+ def handle_event("validate_password", params, socket) do
+ %{"user" => user_params} = params
+
+ password_form =
+ socket.assigns.current_scope.user
+ |> Accounts.change_user_password(user_params, hash_password: false)
+ |> Map.put(:action, :validate)
+ |> to_form()
+
+ {:noreply, assign(socket, password_form: password_form)}
+ end
+
+ def handle_event("update_password", params, socket) do
+ %{"user" => user_params} = params
+ user = socket.assigns.current_scope.user
+ true = Accounts.sudo_mode?(user)
+
+ case Accounts.change_user_password(user, user_params) do
+ %{valid?: true} = changeset ->
+ {:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))}
+
+ changeset ->
+ {:noreply, assign(socket, password_form: to_form(changeset, action: :insert))}
+ end
+ end
+end
diff --git a/lib/daily_tracker_web/router.ex b/lib/daily_tracker_web/router.ex
@@ -0,0 +1,80 @@
+defmodule DailyTrackerWeb.Router do
+ use DailyTrackerWeb, :router
+
+ import DailyTrackerWeb.UserAuth
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, html: {DailyTrackerWeb.Layouts, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ plug :fetch_current_scope_for_user
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", DailyTrackerWeb do
+ pipe_through :browser
+
+ get "/", PageController, :home
+ end
+
+ # Other scopes may use custom stacks.
+ # scope "/api", DailyTrackerWeb do
+ # pipe_through :api
+ # end
+
+ # Enable LiveDashboard and Swoosh mailbox preview in development
+ if Application.compile_env(:daily_tracker, :dev_routes) do
+ # If you want to use the LiveDashboard in production, you should put
+ # it behind authentication and allow only admins to access it.
+ # If your application does not have an admins-only section yet,
+ # you can use Plug.BasicAuth to set up some basic authentication
+ # as long as you are also using SSL (which you should anyway).
+ import Phoenix.LiveDashboard.Router
+
+ scope "/dev" do
+ pipe_through :browser
+
+ live_dashboard "/dashboard", metrics: DailyTrackerWeb.Telemetry
+ forward "/mailbox", Plug.Swoosh.MailboxPreview
+ end
+ end
+
+ ## Authentication routes
+
+ scope "/", DailyTrackerWeb do
+ pipe_through [:browser, :require_authenticated_user]
+
+ live_session :require_authenticated_user,
+ on_mount: [{DailyTrackerWeb.UserAuth, :require_authenticated}] do
+ live "/entries", EntryLive.Index, :index
+ live "/entries/new", EntryLive.Form, :new
+ live "/entries/:id", EntryLive.Show, :show
+ live "/entries/:id/edit", EntryLive.Form, :edit
+
+ live "/users/settings", UserLive.Settings, :edit
+ live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
+ end
+
+ post "/users/update-password", UserSessionController, :update_password
+ end
+
+ scope "/", DailyTrackerWeb do
+ pipe_through [:browser]
+
+ live_session :current_user,
+ on_mount: [{DailyTrackerWeb.UserAuth, :mount_current_scope}] do
+ live "/users/register", UserLive.Registration, :new
+ live "/users/log-in", UserLive.Login, :new
+ live "/users/log-in/:token", UserLive.Confirmation, :new
+ end
+
+ post "/users/log-in", UserSessionController, :create
+ delete "/users/log-out", UserSessionController, :delete
+ end
+end
diff --git a/lib/daily_tracker_web/telemetry.ex b/lib/daily_tracker_web/telemetry.ex
@@ -0,0 +1,93 @@
+defmodule DailyTrackerWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ # Telemetry poller will execute the given period measurements
+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.start.system_time",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.start.system_time",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.exception.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.socket_connected.duration",
+ unit: {:native, :millisecond}
+ ),
+ sum("phoenix.socket_drain.count"),
+ summary("phoenix.channel_joined.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_handled_in.duration",
+ tags: [:event],
+ unit: {:native, :millisecond}
+ ),
+
+ # Database Metrics
+ summary("daily_tracker.repo.query.total_time",
+ unit: {:native, :millisecond},
+ description: "The sum of the other measurements"
+ ),
+ summary("daily_tracker.repo.query.decode_time",
+ unit: {:native, :millisecond},
+ description: "The time spent decoding the data received from the database"
+ ),
+ summary("daily_tracker.repo.query.query_time",
+ unit: {:native, :millisecond},
+ description: "The time spent executing the query"
+ ),
+ summary("daily_tracker.repo.query.queue_time",
+ unit: {:native, :millisecond},
+ description: "The time spent waiting for a database connection"
+ ),
+ summary("daily_tracker.repo.query.idle_time",
+ unit: {:native, :millisecond},
+ description:
+ "The time the connection spent waiting before being checked out for the query"
+ ),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {DailyTrackerWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/lib/daily_tracker_web/user_auth.ex b/lib/daily_tracker_web/user_auth.ex
@@ -0,0 +1,287 @@
+defmodule DailyTrackerWeb.UserAuth do
+ use DailyTrackerWeb, :verified_routes
+
+ import Plug.Conn
+ import Phoenix.Controller
+
+ alias DailyTracker.Accounts
+ alias DailyTracker.Accounts.Scope
+
+ # Make the remember me cookie valid for 14 days. This should match
+ # the session validity setting in UserToken.
+ @max_cookie_age_in_days 14
+ @remember_me_cookie "_daily_tracker_web_user_remember_me"
+ @remember_me_options [
+ sign: true,
+ max_age: @max_cookie_age_in_days * 24 * 60 * 60,
+ same_site: "Lax"
+ ]
+
+ # How old the session token should be before a new one is issued. When a request is made
+ # with a session token older than this value, then a new session token will be created
+ # and the session and remember-me cookies (if set) will be updated with the new token.
+ # Lowering this value will result in more tokens being created by active users. Increasing
+ # it will result in less time before a session token expires for a user to get issued a new
+ # token. This can be set to a value greater than `@max_cookie_age_in_days` to disable
+ # the reissuing of tokens completely.
+ @session_reissue_age_in_days 7
+
+ @doc """
+ Logs the user in.
+
+ Redirects to the session's `:user_return_to` path
+ or falls back to the `signed_in_path/1`.
+ """
+ def log_in_user(conn, user, params \\ %{}) do
+ user_return_to = get_session(conn, :user_return_to)
+
+ conn
+ |> create_or_extend_session(user, params)
+ |> redirect(to: user_return_to || signed_in_path(conn))
+ end
+
+ @doc """
+ Logs the user out.
+
+ It clears all session data for safety. See renew_session.
+ """
+ def log_out_user(conn) do
+ user_token = get_session(conn, :user_token)
+ user_token && Accounts.delete_user_session_token(user_token)
+
+ if live_socket_id = get_session(conn, :live_socket_id) do
+ DailyTrackerWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
+ end
+
+ conn
+ |> renew_session(nil)
+ |> delete_resp_cookie(@remember_me_cookie, @remember_me_options)
+ |> redirect(to: ~p"/")
+ end
+
+ @doc """
+ Authenticates the user by looking into the session and remember me token.
+
+ Will reissue the session token if it is older than the configured age.
+ """
+ def fetch_current_scope_for_user(conn, _opts) do
+ with {token, conn} <- ensure_user_token(conn),
+ {user, token_inserted_at} <- Accounts.get_user_by_session_token(token) do
+ conn
+ |> assign(:current_scope, Scope.for_user(user))
+ |> maybe_reissue_user_session_token(user, token_inserted_at)
+ else
+ nil -> assign(conn, :current_scope, Scope.for_user(nil))
+ end
+ end
+
+ defp ensure_user_token(conn) do
+ if token = get_session(conn, :user_token) do
+ {token, conn}
+ else
+ conn = fetch_cookies(conn, signed: [@remember_me_cookie])
+
+ if token = conn.cookies[@remember_me_cookie] do
+ {token, conn |> put_token_in_session(token) |> put_session(:user_remember_me, true)}
+ else
+ nil
+ end
+ end
+ end
+
+ # Reissue the session token if it is older than the configured reissue age.
+ defp maybe_reissue_user_session_token(conn, user, token_inserted_at) do
+ token_age = DateTime.diff(DateTime.utc_now(:second), token_inserted_at, :day)
+
+ if token_age >= @session_reissue_age_in_days do
+ create_or_extend_session(conn, user, %{})
+ else
+ conn
+ end
+ end
+
+ # This function is the one responsible for creating session tokens
+ # and storing them safely in the session and cookies. It may be called
+ # either when logging in, during sudo mode, or to renew a session which
+ # will soon expire.
+ #
+ # When the session is created, rather than extended, the renew_session
+ # function will clear the session to avoid fixation attacks. See the
+ # renew_session function to customize this behaviour.
+ defp create_or_extend_session(conn, user, params) do
+ token = Accounts.generate_user_session_token(user)
+ remember_me = get_session(conn, :user_remember_me)
+
+ conn
+ |> renew_session(user)
+ |> put_token_in_session(token)
+ |> maybe_write_remember_me_cookie(token, params, remember_me)
+ end
+
+ # Do not renew session if the user is already logged in
+ # to prevent CSRF errors or data being lost in tabs that are still open
+ defp renew_session(conn, user) when conn.assigns.current_scope.user.id == user.id do
+ conn
+ end
+
+ # This function renews the session ID and erases the whole
+ # session to avoid fixation attacks. If there is any data
+ # in the session you may want to preserve after log in/log out,
+ # you must explicitly fetch the session data before clearing
+ # and then immediately set it after clearing, for example:
+ #
+ # defp renew_session(conn, _user) do
+ # delete_csrf_token()
+ # preferred_locale = get_session(conn, :preferred_locale)
+ #
+ # conn
+ # |> configure_session(renew: true)
+ # |> clear_session()
+ # |> put_session(:preferred_locale, preferred_locale)
+ # end
+ #
+ defp renew_session(conn, _user) do
+ delete_csrf_token()
+
+ conn
+ |> configure_session(renew: true)
+ |> clear_session()
+ end
+
+ defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}, _),
+ do: write_remember_me_cookie(conn, token)
+
+ defp maybe_write_remember_me_cookie(conn, token, _params, true),
+ do: write_remember_me_cookie(conn, token)
+
+ defp maybe_write_remember_me_cookie(conn, _token, _params, _), do: conn
+
+ defp write_remember_me_cookie(conn, token) do
+ conn
+ |> put_session(:user_remember_me, true)
+ |> put_resp_cookie(@remember_me_cookie, token, @remember_me_options)
+ end
+
+ defp put_token_in_session(conn, token) do
+ conn
+ |> put_session(:user_token, token)
+ |> put_session(:live_socket_id, user_session_topic(token))
+ end
+
+ @doc """
+ Disconnects existing sockets for the given tokens.
+ """
+ def disconnect_sessions(tokens) do
+ Enum.each(tokens, fn %{token: token} ->
+ DailyTrackerWeb.Endpoint.broadcast(user_session_topic(token), "disconnect", %{})
+ end)
+ end
+
+ defp user_session_topic(token), do: "users_sessions:#{Base.url_encode64(token)}"
+
+ @doc """
+ Handles mounting and authenticating the current_scope in LiveViews.
+
+ ## `on_mount` arguments
+
+ * `:mount_current_scope` - Assigns current_scope
+ to socket assigns based on user_token, or nil if
+ there's no user_token or no matching user.
+
+ * `:require_authenticated` - Authenticates the user from the session,
+ and assigns the current_scope to socket assigns based
+ on user_token.
+ Redirects to login page if there's no logged user.
+
+ ## Examples
+
+ Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
+ the `current_scope`:
+
+ defmodule DailyTrackerWeb.PageLive do
+ use DailyTrackerWeb, :live_view
+
+ on_mount {DailyTrackerWeb.UserAuth, :mount_current_scope}
+ ...
+ end
+
+ Or use the `live_session` of your router to invoke the on_mount callback:
+
+ live_session :authenticated, on_mount: [{DailyTrackerWeb.UserAuth, :require_authenticated}] do
+ live "/profile", ProfileLive, :index
+ end
+ """
+ def on_mount(:mount_current_scope, _params, session, socket) do
+ {:cont, mount_current_scope(socket, session)}
+ end
+
+ def on_mount(:require_authenticated, _params, session, socket) do
+ socket = mount_current_scope(socket, session)
+
+ if socket.assigns.current_scope && socket.assigns.current_scope.user do
+ {:cont, socket}
+ else
+ socket =
+ socket
+ |> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
+ |> Phoenix.LiveView.redirect(to: ~p"/users/log-in")
+
+ {:halt, socket}
+ end
+ end
+
+ def on_mount(:require_sudo_mode, _params, session, socket) do
+ socket = mount_current_scope(socket, session)
+
+ if Accounts.sudo_mode?(socket.assigns.current_scope.user, -10) do
+ {:cont, socket}
+ else
+ socket =
+ socket
+ |> Phoenix.LiveView.put_flash(:error, "You must re-authenticate to access this page.")
+ |> Phoenix.LiveView.redirect(to: ~p"/users/log-in")
+
+ {:halt, socket}
+ end
+ end
+
+ defp mount_current_scope(socket, session) do
+ Phoenix.Component.assign_new(socket, :current_scope, fn ->
+ {user, _} =
+ if user_token = session["user_token"] do
+ Accounts.get_user_by_session_token(user_token)
+ end || {nil, nil}
+
+ Scope.for_user(user)
+ end)
+ end
+
+ @doc "Returns the path to redirect to after log in."
+ # the user was already logged in, redirect to settings
+ def signed_in_path(%Plug.Conn{assigns: %{current_scope: %Scope{user: %Accounts.User{}}}}) do
+ ~p"/users/settings"
+ end
+
+ def signed_in_path(_), do: ~p"/"
+
+ @doc """
+ Plug for routes that require the user to be authenticated.
+ """
+ def require_authenticated_user(conn, _opts) do
+ if conn.assigns.current_scope && conn.assigns.current_scope.user do
+ conn
+ else
+ conn
+ |> put_flash(:error, "You must log in to access this page.")
+ |> maybe_store_return_to()
+ |> redirect(to: ~p"/users/log-in")
+ |> halt()
+ end
+ end
+
+ defp maybe_store_return_to(%{method: "GET"} = conn) do
+ put_session(conn, :user_return_to, current_path(conn))
+ end
+
+ defp maybe_store_return_to(conn), do: conn
+end
diff --git a/mix.exs b/mix.exs
@@ -0,0 +1,96 @@
+defmodule DailyTracker.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :daily_tracker,
+ version: "0.1.0",
+ elixir: "~> 1.15",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps(),
+ compilers: [:phoenix_live_view] ++ Mix.compilers(),
+ listeners: [Phoenix.CodeReloader]
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {DailyTracker.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ def cli do
+ [
+ preferred_envs: [precommit: :test]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:bcrypt_elixir, "~> 3.0"},
+ {:phoenix, "~> 1.8.7"},
+ {:phoenix_ecto, "~> 4.5"},
+ {:ecto_sql, "~> 3.13"},
+ {:postgrex, ">= 0.0.0"},
+ {:phoenix_html, "~> 4.1"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_view, "~> 1.1.0"},
+ {:lazy_html, ">= 0.1.0", only: :test},
+ {:phoenix_live_dashboard, "~> 0.8.3"},
+ {:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
+ {:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
+ {:heroicons,
+ github: "tailwindlabs/heroicons",
+ tag: "v2.2.0",
+ sparse: "optimized",
+ app: false,
+ compile: false,
+ depth: 1},
+ {:swoosh, "~> 1.16"},
+ {:gen_smtp, "~> 1.0"},
+ {:req, "~> 0.5"},
+ {:telemetry_metrics, "~> 1.0"},
+ {:telemetry_poller, "~> 1.0"},
+ {:gettext, "~> 1.0"},
+ {:jason, "~> 1.2"},
+ {:dns_cluster, "~> 0.2.0"},
+ {:bandit, "~> 1.5"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
+ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+ "ecto.reset": ["ecto.drop", "ecto.setup"],
+ test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
+ "assets.build": ["compile", "tailwind daily_tracker", "esbuild daily_tracker"],
+ "assets.deploy": [
+ "tailwind daily_tracker --minify",
+ "esbuild daily_tracker --minify",
+ "phx.digest"
+ ],
+ precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
+ ]
+ end
+end
diff --git a/mix.lock b/mix.lock
@@ -0,0 +1,49 @@
+%{
+ "bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"},
+ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
+ "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
+ "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
+ "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"},
+ "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
+ "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
+ "ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"},
+ "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"},
+ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
+ "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
+ "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
+ "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
+ "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
+ "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
+ "gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
+ "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
+ "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
+ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
+ "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
+ "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
+ "lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
+ "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
+ "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"},
+ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
+ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
+ "phoenix": {:hex, :phoenix, "1.8.7", "d8d755b4ff4b449f610223dd706b4ae64155cb720d3dc09c706c079ecea189e4", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "47352f72d6ab31009ef77516b1b3a14745be97b54061fd458031b9d8294869d5"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
+ "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.31", "c45c85df509dd79c917bc530e26c71299e3920850f65ea52ab6a19ccee66875a", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2f53cc6a9e149f30449341c2775990819d97e3b22338fe719c4d30342e6f9638"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
+ "postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"},
+ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
+ "req": {:hex, :req, "0.5.18", "48e6431cb4135e8a7815e745177485369a9b4a9924d5fe68ca00eb09ceaed1ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996"},
+ "swoosh": {:hex, :swoosh, "1.26.0", "8fb146d261e3c4df2d828df0e28a5f233674976c9464c5177f13b7a431a266a9", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "83fe2c7c6572d21d5cdb47766ea041c13da234d09c96220c480eb90b21b1335c"},
+ "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
+ "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
+ "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
+}
diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po
@@ -0,0 +1,112 @@
+## `msgid`s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove `msgid`s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use `mix gettext.extract --merge` or `mix gettext.merge`
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en\n"
+
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be %{count} byte(s)"
+msgid_plural "should be %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} byte(s)"
+msgid_plural "should be at least %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} byte(s)"
+msgid_plural "should be at most %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""
diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot
@@ -0,0 +1,109 @@
+## This is a PO Template file.
+##
+## `msgid`s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run `mix gettext.extract` to bring this file up to
+## date. Leave `msgstr`s empty as changing them here has no
+## effect: edit them in PO (`.po`) files instead.
+## From Ecto.Changeset.cast/4
+msgid "can't be blank"
+msgstr ""
+
+## From Ecto.Changeset.unique_constraint/3
+msgid "has already been taken"
+msgstr ""
+
+## From Ecto.Changeset.put_change/3
+msgid "is invalid"
+msgstr ""
+
+## From Ecto.Changeset.validate_acceptance/3
+msgid "must be accepted"
+msgstr ""
+
+## From Ecto.Changeset.validate_format/3
+msgid "has invalid format"
+msgstr ""
+
+## From Ecto.Changeset.validate_subset/3
+msgid "has an invalid entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_exclusion/3
+msgid "is reserved"
+msgstr ""
+
+## From Ecto.Changeset.validate_confirmation/3
+msgid "does not match confirmation"
+msgstr ""
+
+## From Ecto.Changeset.no_assoc_constraint/3
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+## From Ecto.Changeset.validate_length/3
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be %{count} byte(s)"
+msgid_plural "should be %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} byte(s)"
+msgid_plural "should be at least %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} byte(s)"
+msgid_plural "should be at most %{count} byte(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+## From Ecto.Changeset.validate_number/3
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""
diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs
@@ -0,0 +1,4 @@
+[
+ import_deps: [:ecto_sql],
+ inputs: ["*.exs"]
+]
diff --git a/priv/repo/migrations/20260530203140_create_entries.exs b/priv/repo/migrations/20260530203140_create_entries.exs
@@ -0,0 +1,19 @@
+defmodule DailyTracker.Repo.Migrations.CreateEntries do
+ use Ecto.Migration
+
+ def change do
+ create table(:entries) do
+ add :date, :date
+ add :mood, :string
+ add :headache, :boolean, default: false, null: false
+ add :water_glasses, :integer
+ add :worked_out, :boolean, default: false, null: false
+ add :bowel_movement, :boolean, default: false, null: false
+ add :hunger_level, :integer
+ add :period, :boolean, default: false, null: false
+ add :notes, :text
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20260530210131_create_users_auth_tables.exs b/priv/repo/migrations/20260530210131_create_users_auth_tables.exs
@@ -0,0 +1,30 @@
+defmodule DailyTracker.Repo.Migrations.CreateUsersAuthTables do
+ use Ecto.Migration
+
+ def change do
+ execute "CREATE EXTENSION IF NOT EXISTS citext", ""
+
+ create table(:users) do
+ add :email, :citext, null: false
+ add :hashed_password, :string
+ add :confirmed_at, :utc_datetime
+
+ timestamps(type: :utc_datetime)
+ end
+
+ create unique_index(:users, [:email])
+
+ create table(:users_tokens) do
+ add :user_id, references(:users, on_delete: :delete_all), null: false
+ add :token, :binary, null: false
+ add :context, :string, null: false
+ add :sent_to, :string
+ add :authenticated_at, :utc_datetime
+
+ timestamps(type: :utc_datetime, updated_at: false)
+ end
+
+ create index(:users_tokens, [:user_id])
+ create unique_index(:users_tokens, [:context, :token])
+ end
+end
diff --git a/priv/repo/migrations/20260530230420_add_user_id_to_entries.exs b/priv/repo/migrations/20260530230420_add_user_id_to_entries.exs
@@ -0,0 +1,11 @@
+defmodule DailyTracker.Repo.Migrations.AddUserIdToEntries do
+ use Ecto.Migration
+
+ def change do
+ alter table(:entries) do
+ add :user_id, references(:users, on_delete: :delete_all), null: false
+ end
+
+ create index(:entries, [:user_id])
+ end
+end
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
@@ -0,0 +1,11 @@
+# Script for populating the database. You can run it as:
+#
+# mix run priv/repo/seeds.exs
+#
+# Inside the script, you can read and write to any of your
+# repositories directly:
+#
+# DailyTracker.Repo.insert!(%DailyTracker.SomeSchema{})
+#
+# We recommend using the bang functions (`insert!`, `update!`
+# and so on) as they will fail if something goes wrong.
diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico
Binary files differ.
diff --git a/priv/static/images/logo.svg b/priv/static/images/logo.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true">
+ <path
+ d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.077.057c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728a13 13 0 0 0 1.182 1.106c1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z"
+ fill="#FD4F00"
+ />
+</svg>
diff --git a/priv/static/robots.txt b/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/test/daily_tracker/accounts_test.exs b/test/daily_tracker/accounts_test.exs
@@ -0,0 +1,397 @@
+defmodule DailyTracker.AccountsTest do
+ use DailyTracker.DataCase
+
+ alias DailyTracker.Accounts
+
+ import DailyTracker.AccountsFixtures
+ alias DailyTracker.Accounts.{User, UserToken}
+
+ describe "get_user_by_email/1" do
+ test "does not return the user if the email does not exist" do
+ refute Accounts.get_user_by_email("unknown@example.com")
+ end
+
+ test "returns the user if the email exists" do
+ %{id: id} = user = user_fixture()
+ assert %User{id: ^id} = Accounts.get_user_by_email(user.email)
+ end
+ end
+
+ describe "get_user_by_email_and_password/2" do
+ test "does not return the user if the email does not exist" do
+ refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
+ end
+
+ test "does not return the user if the password is not valid" do
+ user = user_fixture() |> set_password()
+ refute Accounts.get_user_by_email_and_password(user.email, "invalid")
+ end
+
+ test "returns the user if the email and password are valid" do
+ %{id: id} = user = user_fixture() |> set_password()
+
+ assert %User{id: ^id} =
+ Accounts.get_user_by_email_and_password(user.email, valid_user_password())
+ end
+ end
+
+ describe "get_user!/1" do
+ test "raises if id is invalid" do
+ assert_raise Ecto.NoResultsError, fn ->
+ Accounts.get_user!(-1)
+ end
+ end
+
+ test "returns the user with the given id" do
+ %{id: id} = user = user_fixture()
+ assert %User{id: ^id} = Accounts.get_user!(user.id)
+ end
+ end
+
+ describe "register_user/1" do
+ test "requires email to be set" do
+ {:error, changeset} = Accounts.register_user(%{})
+
+ assert %{email: ["can't be blank"]} = errors_on(changeset)
+ end
+
+ test "validates email when given" do
+ {:error, changeset} = Accounts.register_user(%{email: "not valid"})
+
+ assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
+ end
+
+ test "validates maximum values for email for security" do
+ too_long = String.duplicate("db", 100)
+ {:error, changeset} = Accounts.register_user(%{email: too_long})
+ assert "should be at most 160 character(s)" in errors_on(changeset).email
+ end
+
+ test "validates email uniqueness" do
+ %{email: email} = user_fixture()
+ {:error, changeset} = Accounts.register_user(%{email: email})
+ assert "has already been taken" in errors_on(changeset).email
+
+ # Now try with the uppercased email too, to check that email case is ignored.
+ {:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
+ assert "has already been taken" in errors_on(changeset).email
+ end
+
+ test "registers users without password" do
+ email = unique_user_email()
+ {:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
+ assert user.email == email
+ assert is_nil(user.hashed_password)
+ assert is_nil(user.confirmed_at)
+ assert is_nil(user.password)
+ end
+ end
+
+ describe "sudo_mode?/2" do
+ test "validates the authenticated_at time" do
+ now = DateTime.utc_now()
+
+ assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.utc_now()})
+ assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -19, :minute)})
+ refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -21, :minute)})
+
+ # minute override
+ refute Accounts.sudo_mode?(
+ %User{authenticated_at: DateTime.add(now, -11, :minute)},
+ -10
+ )
+
+ # not authenticated
+ refute Accounts.sudo_mode?(%User{})
+ end
+ end
+
+ describe "change_user_email/3" do
+ test "returns a user changeset" do
+ assert %Ecto.Changeset{} = changeset = Accounts.change_user_email(%User{})
+ assert changeset.required == [:email]
+ end
+ end
+
+ describe "deliver_user_update_email_instructions/3" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "sends token through notification", %{user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
+ end)
+
+ {:ok, token} = Base.url_decode64(token, padding: false)
+ assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
+ assert user_token.user_id == user.id
+ assert user_token.sent_to == user.email
+ assert user_token.context == "change:current@example.com"
+ end
+ end
+
+ describe "update_user_email/2" do
+ setup do
+ user = unconfirmed_user_fixture()
+ email = unique_user_email()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
+ end)
+
+ %{user: user, token: token, email: email}
+ end
+
+ test "updates the email with a valid token", %{user: user, token: token, email: email} do
+ assert {:ok, %{email: ^email}} = Accounts.update_user_email(user, token)
+ changed_user = Repo.get!(User, user.id)
+ assert changed_user.email != user.email
+ assert changed_user.email == email
+ refute Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not update email with invalid token", %{user: user} do
+ assert Accounts.update_user_email(user, "oops") ==
+ {:error, :transaction_aborted}
+
+ assert Repo.get!(User, user.id).email == user.email
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not update email if user email changed", %{user: user, token: token} do
+ assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) ==
+ {:error, :transaction_aborted}
+
+ assert Repo.get!(User, user.id).email == user.email
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+
+ test "does not update email if token expired", %{user: user, token: token} do
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+
+ assert Accounts.update_user_email(user, token) ==
+ {:error, :transaction_aborted}
+
+ assert Repo.get!(User, user.id).email == user.email
+ assert Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "change_user_password/3" do
+ test "returns a user changeset" do
+ assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{})
+ assert changeset.required == [:password]
+ end
+
+ test "allows fields to be set" do
+ changeset =
+ Accounts.change_user_password(
+ %User{},
+ %{
+ "password" => "new valid password"
+ },
+ hash_password: false
+ )
+
+ assert changeset.valid?
+ assert get_change(changeset, :password) == "new valid password"
+ assert is_nil(get_change(changeset, :hashed_password))
+ end
+ end
+
+ describe "update_user_password/2" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "validates password", %{user: user} do
+ {:error, changeset} =
+ Accounts.update_user_password(user, %{
+ password: "not valid",
+ password_confirmation: "another"
+ })
+
+ assert %{
+ password: ["should be at least 12 character(s)"],
+ password_confirmation: ["does not match password"]
+ } = errors_on(changeset)
+ end
+
+ test "validates maximum values for password for security", %{user: user} do
+ too_long = String.duplicate("db", 100)
+
+ {:error, changeset} =
+ Accounts.update_user_password(user, %{password: too_long})
+
+ assert "should be at most 72 character(s)" in errors_on(changeset).password
+ end
+
+ test "updates the password", %{user: user} do
+ {:ok, {user, expired_tokens}} =
+ Accounts.update_user_password(user, %{
+ password: "new valid password"
+ })
+
+ assert expired_tokens == []
+ assert is_nil(user.password)
+ assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
+ end
+
+ test "deletes all tokens for the given user", %{user: user} do
+ _ = Accounts.generate_user_session_token(user)
+
+ {:ok, {_, _}} =
+ Accounts.update_user_password(user, %{
+ password: "new valid password"
+ })
+
+ refute Repo.get_by(UserToken, user_id: user.id)
+ end
+ end
+
+ describe "generate_user_session_token/1" do
+ setup do
+ %{user: user_fixture()}
+ end
+
+ test "generates a token", %{user: user} do
+ token = Accounts.generate_user_session_token(user)
+ assert user_token = Repo.get_by(UserToken, token: token)
+ assert user_token.context == "session"
+ assert user_token.authenticated_at != nil
+
+ # Creating the same token for another user should fail
+ assert_raise Ecto.ConstraintError, fn ->
+ Repo.insert!(%UserToken{
+ token: user_token.token,
+ user_id: user_fixture().id,
+ context: "session"
+ })
+ end
+ end
+
+ test "duplicates the authenticated_at of given user in new token", %{user: user} do
+ user = %{user | authenticated_at: DateTime.add(DateTime.utc_now(:second), -3600)}
+ token = Accounts.generate_user_session_token(user)
+ assert user_token = Repo.get_by(UserToken, token: token)
+ assert user_token.authenticated_at == user.authenticated_at
+ assert DateTime.compare(user_token.inserted_at, user.authenticated_at) == :gt
+ end
+ end
+
+ describe "get_user_by_session_token/1" do
+ setup do
+ user = user_fixture()
+ token = Accounts.generate_user_session_token(user)
+ %{user: user, token: token}
+ end
+
+ test "returns user by token", %{user: user, token: token} do
+ assert {session_user, token_inserted_at} = Accounts.get_user_by_session_token(token)
+ assert session_user.id == user.id
+ assert session_user.authenticated_at != nil
+ assert token_inserted_at != nil
+ end
+
+ test "does not return user for invalid token" do
+ refute Accounts.get_user_by_session_token("oops")
+ end
+
+ test "does not return user for expired token", %{token: token} do
+ dt = ~N[2020-01-01 00:00:00]
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: dt, authenticated_at: dt])
+ refute Accounts.get_user_by_session_token(token)
+ end
+ end
+
+ describe "get_user_by_magic_link_token/1" do
+ setup do
+ user = user_fixture()
+ {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
+ %{user: user, token: encoded_token}
+ end
+
+ test "returns user by token", %{user: user, token: token} do
+ assert session_user = Accounts.get_user_by_magic_link_token(token)
+ assert session_user.id == user.id
+ end
+
+ test "does not return user for invalid token" do
+ refute Accounts.get_user_by_magic_link_token("oops")
+ end
+
+ test "does not return user for expired token", %{token: token} do
+ {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+ refute Accounts.get_user_by_magic_link_token(token)
+ end
+ end
+
+ describe "login_user_by_magic_link/1" do
+ test "confirms user and expires tokens" do
+ user = unconfirmed_user_fixture()
+ refute user.confirmed_at
+ {encoded_token, hashed_token} = generate_user_magic_link_token(user)
+
+ assert {:ok, {user, [%{token: ^hashed_token}]}} =
+ Accounts.login_user_by_magic_link(encoded_token)
+
+ assert user.confirmed_at
+ end
+
+ test "returns user and (deleted) token for confirmed user" do
+ user = user_fixture()
+ assert user.confirmed_at
+ {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
+ assert {:ok, {^user, []}} = Accounts.login_user_by_magic_link(encoded_token)
+ # one time use only
+ assert {:error, :not_found} = Accounts.login_user_by_magic_link(encoded_token)
+ end
+
+ test "raises when unconfirmed user has password set" do
+ user = unconfirmed_user_fixture()
+ {1, nil} = Repo.update_all(User, set: [hashed_password: "hashed"])
+ {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
+
+ assert_raise RuntimeError, ~r/magic link log in is not allowed/, fn ->
+ Accounts.login_user_by_magic_link(encoded_token)
+ end
+ end
+ end
+
+ describe "delete_user_session_token/1" do
+ test "deletes the token" do
+ user = user_fixture()
+ token = Accounts.generate_user_session_token(user)
+ assert Accounts.delete_user_session_token(token) == :ok
+ refute Accounts.get_user_by_session_token(token)
+ end
+ end
+
+ describe "deliver_login_instructions/2" do
+ setup do
+ %{user: unconfirmed_user_fixture()}
+ end
+
+ test "sends token through notification", %{user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, token} = Base.url_decode64(token, padding: false)
+ assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
+ assert user_token.user_id == user.id
+ assert user_token.sent_to == user.email
+ assert user_token.context == "login"
+ end
+ end
+
+ describe "inspect/2 for the User module" do
+ test "does not include password" do
+ refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
+ end
+ end
+end
diff --git a/test/daily_tracker/tracker_test.exs b/test/daily_tracker/tracker_test.exs
@@ -0,0 +1,75 @@
+defmodule DailyTracker.TrackerTest do
+ use DailyTracker.DataCase
+
+ alias DailyTracker.Tracker
+
+ describe "entries" do
+ alias DailyTracker.Tracker.Entry
+
+ import DailyTracker.TrackerFixtures
+
+ @invalid_attrs %{date: nil, period: nil, mood: nil, headache: nil, water_glasses: nil, worked_out: nil, bowel_movement: nil, hunger_level: nil, notes: nil}
+
+ test "list_entries/0 returns all entries" do
+ entry = entry_fixture()
+ assert Tracker.list_entries() == [entry]
+ end
+
+ test "get_entry!/1 returns the entry with given id" do
+ entry = entry_fixture()
+ assert Tracker.get_entry!(entry.id) == entry
+ end
+
+ test "create_entry/1 with valid data creates a entry" do
+ valid_attrs = %{date: ~D[2026-05-29], period: true, mood: "some mood", headache: true, water_glasses: 42, worked_out: true, bowel_movement: true, hunger_level: 42, notes: "some notes"}
+
+ assert {:ok, %Entry{} = entry} = Tracker.create_entry(valid_attrs)
+ assert entry.date == ~D[2026-05-29]
+ assert entry.period == true
+ assert entry.mood == "some mood"
+ assert entry.headache == true
+ assert entry.water_glasses == 42
+ assert entry.worked_out == true
+ assert entry.bowel_movement == true
+ assert entry.hunger_level == 42
+ assert entry.notes == "some notes"
+ end
+
+ test "create_entry/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Tracker.create_entry(@invalid_attrs)
+ end
+
+ test "update_entry/2 with valid data updates the entry" do
+ entry = entry_fixture()
+ update_attrs = %{date: ~D[2026-05-30], period: false, mood: "some updated mood", headache: false, water_glasses: 43, worked_out: false, bowel_movement: false, hunger_level: 43, notes: "some updated notes"}
+
+ assert {:ok, %Entry{} = entry} = Tracker.update_entry(entry, update_attrs)
+ assert entry.date == ~D[2026-05-30]
+ assert entry.period == false
+ assert entry.mood == "some updated mood"
+ assert entry.headache == false
+ assert entry.water_glasses == 43
+ assert entry.worked_out == false
+ assert entry.bowel_movement == false
+ assert entry.hunger_level == 43
+ assert entry.notes == "some updated notes"
+ end
+
+ test "update_entry/2 with invalid data returns error changeset" do
+ entry = entry_fixture()
+ assert {:error, %Ecto.Changeset{}} = Tracker.update_entry(entry, @invalid_attrs)
+ assert entry == Tracker.get_entry!(entry.id)
+ end
+
+ test "delete_entry/1 deletes the entry" do
+ entry = entry_fixture()
+ assert {:ok, %Entry{}} = Tracker.delete_entry(entry)
+ assert_raise Ecto.NoResultsError, fn -> Tracker.get_entry!(entry.id) end
+ end
+
+ test "change_entry/1 returns a entry changeset" do
+ entry = entry_fixture()
+ assert %Ecto.Changeset{} = Tracker.change_entry(entry)
+ end
+ end
+end
diff --git a/test/daily_tracker_web/controllers/error_html_test.exs b/test/daily_tracker_web/controllers/error_html_test.exs
@@ -0,0 +1,14 @@
+defmodule DailyTrackerWeb.ErrorHTMLTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ # Bring render_to_string/4 for testing custom views
+ import Phoenix.Template, only: [render_to_string: 4]
+
+ test "renders 404.html" do
+ assert render_to_string(DailyTrackerWeb.ErrorHTML, "404", "html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(DailyTrackerWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
+ end
+end
diff --git a/test/daily_tracker_web/controllers/error_json_test.exs b/test/daily_tracker_web/controllers/error_json_test.exs
@@ -0,0 +1,12 @@
+defmodule DailyTrackerWeb.ErrorJSONTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ test "renders 404" do
+ assert DailyTrackerWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
+ end
+
+ test "renders 500" do
+ assert DailyTrackerWeb.ErrorJSON.render("500.json", %{}) ==
+ %{errors: %{detail: "Internal Server Error"}}
+ end
+end
diff --git a/test/daily_tracker_web/controllers/page_controller_test.exs b/test/daily_tracker_web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule DailyTrackerWeb.PageControllerTest do
+ use DailyTrackerWeb.ConnCase
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, ~p"/")
+ assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ end
+end
diff --git a/test/daily_tracker_web/controllers/user_session_controller_test.exs b/test/daily_tracker_web/controllers/user_session_controller_test.exs
@@ -0,0 +1,147 @@
+defmodule DailyTrackerWeb.UserSessionControllerTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ import DailyTracker.AccountsFixtures
+ alias DailyTracker.Accounts
+
+ setup do
+ %{unconfirmed_user: unconfirmed_user_fixture(), user: user_fixture()}
+ end
+
+ describe "POST /users/log-in - email and password" do
+ test "logs the user in", %{conn: conn, user: user} do
+ user = set_password(user)
+
+ conn =
+ post(conn, ~p"/users/log-in", %{
+ "user" => %{"email" => user.email, "password" => valid_user_password()}
+ })
+
+ assert get_session(conn, :user_token)
+ assert redirected_to(conn) == ~p"/"
+
+ # Now do a logged in request and assert on the menu
+ conn = get(conn, ~p"/")
+ response = html_response(conn, 200)
+ assert response =~ user.email
+ assert response =~ ~p"/users/settings"
+ assert response =~ ~p"/users/log-out"
+ end
+
+ test "logs the user in with remember me", %{conn: conn, user: user} do
+ user = set_password(user)
+
+ conn =
+ post(conn, ~p"/users/log-in", %{
+ "user" => %{
+ "email" => user.email,
+ "password" => valid_user_password(),
+ "remember_me" => "true"
+ }
+ })
+
+ assert conn.resp_cookies["_daily_tracker_web_user_remember_me"]
+ assert redirected_to(conn) == ~p"/"
+ end
+
+ test "logs the user in with return to", %{conn: conn, user: user} do
+ user = set_password(user)
+
+ conn =
+ conn
+ |> init_test_session(user_return_to: "/foo/bar")
+ |> post(~p"/users/log-in", %{
+ "user" => %{
+ "email" => user.email,
+ "password" => valid_user_password()
+ }
+ })
+
+ assert redirected_to(conn) == "/foo/bar"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
+ end
+
+ test "redirects to login page with invalid credentials", %{conn: conn, user: user} do
+ conn =
+ post(conn, ~p"/users/log-in?mode=password", %{
+ "user" => %{"email" => user.email, "password" => "invalid_password"}
+ })
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
+ assert redirected_to(conn) == ~p"/users/log-in"
+ end
+ end
+
+ describe "POST /users/log-in - magic link" do
+ test "logs the user in", %{conn: conn, user: user} do
+ {token, _hashed_token} = generate_user_magic_link_token(user)
+
+ conn =
+ post(conn, ~p"/users/log-in", %{
+ "user" => %{"token" => token}
+ })
+
+ assert get_session(conn, :user_token)
+ assert redirected_to(conn) == ~p"/"
+
+ # Now do a logged in request and assert on the menu
+ conn = get(conn, ~p"/")
+ response = html_response(conn, 200)
+ assert response =~ user.email
+ assert response =~ ~p"/users/settings"
+ assert response =~ ~p"/users/log-out"
+ end
+
+ test "confirms unconfirmed user", %{conn: conn, unconfirmed_user: user} do
+ {token, _hashed_token} = generate_user_magic_link_token(user)
+ refute user.confirmed_at
+
+ conn =
+ post(conn, ~p"/users/log-in", %{
+ "user" => %{"token" => token},
+ "_action" => "confirmed"
+ })
+
+ assert get_session(conn, :user_token)
+ assert redirected_to(conn) == ~p"/"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "User confirmed successfully."
+
+ assert Accounts.get_user!(user.id).confirmed_at
+
+ # Now do a logged in request and assert on the menu
+ conn = get(conn, ~p"/")
+ response = html_response(conn, 200)
+ assert response =~ user.email
+ assert response =~ ~p"/users/settings"
+ assert response =~ ~p"/users/log-out"
+ end
+
+ test "redirects to login page when magic link is invalid", %{conn: conn} do
+ conn =
+ post(conn, ~p"/users/log-in", %{
+ "user" => %{"token" => "invalid"}
+ })
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+ "The link is invalid or it has expired."
+
+ assert redirected_to(conn) == ~p"/users/log-in"
+ end
+ end
+
+ describe "DELETE /users/log-out" do
+ test "logs the user out", %{conn: conn, user: user} do
+ conn = conn |> log_in_user(user) |> delete(~p"/users/log-out")
+ assert redirected_to(conn) == ~p"/"
+ refute get_session(conn, :user_token)
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
+ end
+
+ test "succeeds even if the user is not logged in", %{conn: conn} do
+ conn = delete(conn, ~p"/users/log-out")
+ assert redirected_to(conn) == ~p"/"
+ refute get_session(conn, :user_token)
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
+ end
+ end
+end
diff --git a/test/daily_tracker_web/live/entry_live_test.exs b/test/daily_tracker_web/live/entry_live_test.exs
@@ -0,0 +1,122 @@
+defmodule DailyTrackerWeb.EntryLiveTest do
+ use DailyTrackerWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import DailyTracker.TrackerFixtures
+
+ @create_attrs %{date: "2026-05-29", period: true, mood: "some mood", headache: true, water_glasses: 42, worked_out: true, bowel_movement: true, hunger_level: 42, notes: "some notes"}
+ @update_attrs %{date: "2026-05-30", period: false, mood: "some updated mood", headache: false, water_glasses: 43, worked_out: false, bowel_movement: false, hunger_level: 43, notes: "some updated notes"}
+ @invalid_attrs %{date: nil, period: false, mood: nil, headache: false, water_glasses: nil, worked_out: false, bowel_movement: false, hunger_level: nil, notes: nil}
+ defp create_entry(_) do
+ entry = entry_fixture()
+
+ %{entry: entry}
+ end
+
+ describe "Index" do
+ setup [:create_entry]
+
+ test "lists all entries", %{conn: conn, entry: entry} do
+ {:ok, _index_live, html} = live(conn, ~p"/entries")
+
+ assert html =~ "Listing Entries"
+ assert html =~ entry.mood
+ end
+
+ test "saves new entry", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/entries")
+
+ assert {:ok, form_live, _} =
+ index_live
+ |> element("a", "New Entry")
+ |> render_click()
+ |> follow_redirect(conn, ~p"/entries/new")
+
+ assert render(form_live) =~ "New Entry"
+
+ assert form_live
+ |> form("#entry-form", entry: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert {:ok, index_live, _html} =
+ form_live
+ |> form("#entry-form", entry: @create_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/entries")
+
+ html = render(index_live)
+ assert html =~ "Entry created successfully"
+ assert html =~ "some mood"
+ end
+
+ test "updates entry in listing", %{conn: conn, entry: entry} do
+ {:ok, index_live, _html} = live(conn, ~p"/entries")
+
+ assert {:ok, form_live, _html} =
+ index_live
+ |> element("#entries-#{entry.id} a", "Edit")
+ |> render_click()
+ |> follow_redirect(conn, ~p"/entries/#{entry}/edit")
+
+ assert render(form_live) =~ "Edit Entry"
+
+ assert form_live
+ |> form("#entry-form", entry: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert {:ok, index_live, _html} =
+ form_live
+ |> form("#entry-form", entry: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/entries")
+
+ html = render(index_live)
+ assert html =~ "Entry updated successfully"
+ assert html =~ "some updated mood"
+ end
+
+ test "deletes entry in listing", %{conn: conn, entry: entry} do
+ {:ok, index_live, _html} = live(conn, ~p"/entries")
+
+ assert index_live |> element("#entries-#{entry.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#entries-#{entry.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_entry]
+
+ test "displays entry", %{conn: conn, entry: entry} do
+ {:ok, _show_live, html} = live(conn, ~p"/entries/#{entry}")
+
+ assert html =~ "Show Entry"
+ assert html =~ entry.mood
+ end
+
+ test "updates entry and returns to show", %{conn: conn, entry: entry} do
+ {:ok, show_live, _html} = live(conn, ~p"/entries/#{entry}")
+
+ assert {:ok, form_live, _} =
+ show_live
+ |> element("a", "Edit")
+ |> render_click()
+ |> follow_redirect(conn, ~p"/entries/#{entry}/edit?return_to=show")
+
+ assert render(form_live) =~ "Edit Entry"
+
+ assert form_live
+ |> form("#entry-form", entry: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert {:ok, show_live, _html} =
+ form_live
+ |> form("#entry-form", entry: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/entries/#{entry}")
+
+ html = render(show_live)
+ assert html =~ "Entry updated successfully"
+ assert html =~ "some updated mood"
+ end
+ end
+end
diff --git a/test/daily_tracker_web/live/user_live/confirmation_test.exs b/test/daily_tracker_web/live/user_live/confirmation_test.exs
@@ -0,0 +1,118 @@
+defmodule DailyTrackerWeb.UserLive.ConfirmationTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ import Phoenix.LiveViewTest
+ import DailyTracker.AccountsFixtures
+
+ alias DailyTracker.Accounts
+
+ setup do
+ %{unconfirmed_user: unconfirmed_user_fixture(), confirmed_user: user_fixture()}
+ end
+
+ describe "Confirm user" do
+ test "renders confirmation page for unconfirmed user", %{conn: conn, unconfirmed_user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
+ assert html =~ "Confirm and stay logged in"
+ end
+
+ test "renders login page for confirmed user", %{conn: conn, confirmed_user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
+ refute html =~ "Confirm my account"
+ assert html =~ "Keep me logged in on this device"
+ end
+
+ test "renders login page for already logged in user", %{conn: conn, confirmed_user: user} do
+ conn = log_in_user(conn, user)
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, _lv, html} = live(conn, ~p"/users/log-in/#{token}")
+ refute html =~ "Confirm my account"
+ assert html =~ "Log in"
+ end
+
+ test "confirms the given token once", %{conn: conn, unconfirmed_user: user} do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in/#{token}")
+
+ form = form(lv, "#confirmation_form", %{"user" => %{"token" => token}})
+ render_submit(form)
+
+ conn = follow_trigger_action(form, conn)
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+ "User confirmed successfully"
+
+ assert Accounts.get_user!(user.id).confirmed_at
+ # we are logged in now
+ assert get_session(conn, :user_token)
+ assert redirected_to(conn) == ~p"/"
+
+ # log out, new conn
+ conn = build_conn()
+
+ {:ok, _lv, html} =
+ live(conn, ~p"/users/log-in/#{token}")
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert html =~ "Magic link is invalid or it has expired"
+ end
+
+ test "logs confirmed user in without changing confirmed_at", %{
+ conn: conn,
+ confirmed_user: user
+ } do
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in/#{token}")
+
+ form = form(lv, "#login_form", %{"user" => %{"token" => token}})
+ render_submit(form)
+
+ conn = follow_trigger_action(form, conn)
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+ "Welcome back!"
+
+ assert Accounts.get_user!(user.id).confirmed_at == user.confirmed_at
+
+ # log out, new conn
+ conn = build_conn()
+
+ {:ok, _lv, html} =
+ live(conn, ~p"/users/log-in/#{token}")
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert html =~ "Magic link is invalid or it has expired"
+ end
+
+ test "raises error for invalid token", %{conn: conn} do
+ {:ok, _lv, html} =
+ live(conn, ~p"/users/log-in/invalid-token")
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert html =~ "Magic link is invalid or it has expired"
+ end
+ end
+end
diff --git a/test/daily_tracker_web/live/user_live/login_test.exs b/test/daily_tracker_web/live/user_live/login_test.exs
@@ -0,0 +1,109 @@
+defmodule DailyTrackerWeb.UserLive.LoginTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ import Phoenix.LiveViewTest
+ import DailyTracker.AccountsFixtures
+
+ describe "login page" do
+ test "renders login page", %{conn: conn} do
+ {:ok, _lv, html} = live(conn, ~p"/users/log-in")
+
+ assert html =~ "Log in"
+ assert html =~ "Register"
+ assert html =~ "Log in with email"
+ end
+ end
+
+ describe "user login - magic link" do
+ test "sends magic link email when user exists", %{conn: conn} do
+ user = user_fixture()
+
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in")
+
+ {:ok, _lv, html} =
+ form(lv, "#login_form_magic", user: %{email: user.email})
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert html =~ "If your email is in our system"
+
+ assert DailyTracker.Repo.get_by!(DailyTracker.Accounts.UserToken, user_id: user.id).context ==
+ "login"
+ end
+
+ test "does not disclose if user is registered", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in")
+
+ {:ok, _lv, html} =
+ form(lv, "#login_form_magic", user: %{email: "idonotexist@example.com"})
+ |> render_submit()
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert html =~ "If your email is in our system"
+ end
+ end
+
+ describe "user login - password" do
+ test "redirects if user logs in with valid credentials", %{conn: conn} do
+ user = user_fixture() |> set_password()
+
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in")
+
+ form =
+ form(lv, "#login_form_password",
+ user: %{email: user.email, password: valid_user_password(), remember_me: true}
+ )
+
+ conn = submit_form(form, conn)
+
+ assert redirected_to(conn) == ~p"/"
+ end
+
+ test "redirects to login page with a flash error if credentials are invalid", %{
+ conn: conn
+ } do
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in")
+
+ form =
+ form(lv, "#login_form_password", user: %{email: "test@email.com", password: "123456"})
+
+ render_submit(form, %{user: %{remember_me: true}})
+
+ conn = follow_trigger_action(form, conn)
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
+ assert redirected_to(conn) == ~p"/users/log-in"
+ end
+ end
+
+ describe "login navigation" do
+ test "redirects to registration page when the Register button is clicked", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/log-in")
+
+ {:ok, _login_live, login_html} =
+ lv
+ |> element("main a", "Sign up")
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/register")
+
+ assert login_html =~ "Register"
+ end
+ end
+
+ describe "re-authentication (sudo mode)" do
+ setup %{conn: conn} do
+ user = user_fixture()
+ %{user: user, conn: log_in_user(conn, user)}
+ end
+
+ test "shows login page with email filled in", %{conn: conn, user: user} do
+ {:ok, _lv, html} = live(conn, ~p"/users/log-in")
+
+ assert html =~ "You need to reauthenticate"
+ refute html =~ "Register"
+ assert html =~ "Log in with email"
+
+ assert html =~
+ ~s(<input type="email" name="user[email]" id="login_form_magic_email" value="#{user.email}")
+ end
+ end
+end
diff --git a/test/daily_tracker_web/live/user_live/registration_test.exs b/test/daily_tracker_web/live/user_live/registration_test.exs
@@ -0,0 +1,82 @@
+defmodule DailyTrackerWeb.UserLive.RegistrationTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ import Phoenix.LiveViewTest
+ import DailyTracker.AccountsFixtures
+
+ describe "Registration page" do
+ test "renders registration page", %{conn: conn} do
+ {:ok, _lv, html} = live(conn, ~p"/users/register")
+
+ assert html =~ "Register"
+ assert html =~ "Log in"
+ end
+
+ test "redirects if already logged in", %{conn: conn} do
+ result =
+ conn
+ |> log_in_user(user_fixture())
+ |> live(~p"/users/register")
+ |> follow_redirect(conn, ~p"/")
+
+ assert {:ok, _conn} = result
+ end
+
+ test "renders errors for invalid data", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ result =
+ lv
+ |> element("#registration_form")
+ |> render_change(user: %{"email" => "with spaces"})
+
+ assert result =~ "Register"
+ assert result =~ "must have the @ sign and no spaces"
+ end
+ end
+
+ describe "register user" do
+ test "creates account but does not log in", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ email = unique_user_email()
+ form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
+
+ {:ok, _lv, html} =
+ render_submit(form)
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert html =~
+ ~r/An email was sent to .*, please access it to confirm your account/
+ end
+
+ test "renders errors for duplicated email", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ user = user_fixture(%{email: "test@email.com"})
+
+ result =
+ lv
+ |> form("#registration_form",
+ user: %{"email" => user.email}
+ )
+ |> render_submit()
+
+ assert result =~ "has already been taken"
+ end
+ end
+
+ describe "registration navigation" do
+ test "redirects to login page when the Log in button is clicked", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/register")
+
+ {:ok, _login_live, login_html} =
+ lv
+ |> element("main a", "Log in")
+ |> render_click()
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert login_html =~ "Log in"
+ end
+ end
+end
diff --git a/test/daily_tracker_web/live/user_live/settings_test.exs b/test/daily_tracker_web/live/user_live/settings_test.exs
@@ -0,0 +1,212 @@
+defmodule DailyTrackerWeb.UserLive.SettingsTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ alias DailyTracker.Accounts
+ import Phoenix.LiveViewTest
+ import DailyTracker.AccountsFixtures
+
+ describe "Settings page" do
+ test "renders settings page", %{conn: conn} do
+ {:ok, _lv, html} =
+ conn
+ |> log_in_user(user_fixture())
+ |> live(~p"/users/settings")
+
+ assert html =~ "Change Email"
+ assert html =~ "Save Password"
+ end
+
+ test "redirects if user is not logged in", %{conn: conn} do
+ assert {:error, redirect} = live(conn, ~p"/users/settings")
+
+ assert {:redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/log-in"
+ assert %{"error" => "You must log in to access this page."} = flash
+ end
+
+ test "redirects if user is not in sudo mode", %{conn: conn} do
+ {:ok, conn} =
+ conn
+ |> log_in_user(user_fixture(),
+ token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -11, :minute)
+ )
+ |> live(~p"/users/settings")
+ |> follow_redirect(conn, ~p"/users/log-in")
+
+ assert conn.resp_body =~ "You must re-authenticate to access this page."
+ end
+ end
+
+ describe "update email form" do
+ setup %{conn: conn} do
+ user = user_fixture()
+ %{conn: log_in_user(conn, user), user: user}
+ end
+
+ test "updates the user email", %{conn: conn, user: user} do
+ new_email = unique_user_email()
+
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> form("#email_form", %{
+ "user" => %{"email" => new_email}
+ })
+ |> render_submit()
+
+ assert result =~ "A link to confirm your email"
+ assert Accounts.get_user_by_email(user.email)
+ end
+
+ test "renders errors with invalid data (phx-change)", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> element("#email_form")
+ |> render_change(%{
+ "action" => "update_email",
+ "user" => %{"email" => "with spaces"}
+ })
+
+ assert result =~ "Change Email"
+ assert result =~ "must have the @ sign and no spaces"
+ end
+
+ test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> form("#email_form", %{
+ "user" => %{"email" => user.email}
+ })
+ |> render_submit()
+
+ assert result =~ "Change Email"
+ assert result =~ "did not change"
+ end
+ end
+
+ describe "update password form" do
+ setup %{conn: conn} do
+ user = user_fixture()
+ %{conn: log_in_user(conn, user), user: user}
+ end
+
+ test "updates the user password", %{conn: conn, user: user} do
+ new_password = valid_user_password()
+
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ form =
+ form(lv, "#password_form", %{
+ "user" => %{
+ "email" => user.email,
+ "password" => new_password,
+ "password_confirmation" => new_password
+ }
+ })
+
+ render_submit(form)
+
+ new_password_conn = follow_trigger_action(form, conn)
+
+ assert redirected_to(new_password_conn) == ~p"/users/settings"
+
+ assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
+
+ assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
+ "Password updated successfully"
+
+ assert Accounts.get_user_by_email_and_password(user.email, new_password)
+ end
+
+ test "renders errors with invalid data (phx-change)", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> element("#password_form")
+ |> render_change(%{
+ "user" => %{
+ "password" => "too short",
+ "password_confirmation" => "does not match"
+ }
+ })
+
+ assert result =~ "Save Password"
+ assert result =~ "should be at least 12 character(s)"
+ assert result =~ "does not match password"
+ end
+
+ test "renders errors with invalid data (phx-submit)", %{conn: conn} do
+ {:ok, lv, _html} = live(conn, ~p"/users/settings")
+
+ result =
+ lv
+ |> form("#password_form", %{
+ "user" => %{
+ "password" => "too short",
+ "password_confirmation" => "does not match"
+ }
+ })
+ |> render_submit()
+
+ assert result =~ "Save Password"
+ assert result =~ "should be at least 12 character(s)"
+ assert result =~ "does not match password"
+ end
+ end
+
+ describe "confirm email" do
+ setup %{conn: conn} do
+ user = user_fixture()
+ email = unique_user_email()
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
+ end)
+
+ %{conn: log_in_user(conn, user), token: token, email: email, user: user}
+ end
+
+ test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
+
+ assert {:live_redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/settings"
+ assert %{"info" => message} = flash
+ assert message == "Email changed successfully."
+ refute Accounts.get_user_by_email(user.email)
+ assert Accounts.get_user_by_email(email)
+
+ # use confirm token again
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
+ assert {:live_redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/settings"
+ assert %{"error" => message} = flash
+ assert message == "Email change link is invalid or it has expired."
+ end
+
+ test "does not update email with invalid token", %{conn: conn, user: user} do
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/oops")
+ assert {:live_redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/settings"
+ assert %{"error" => message} = flash
+ assert message == "Email change link is invalid or it has expired."
+ assert Accounts.get_user_by_email(user.email)
+ end
+
+ test "redirects if user is not logged in", %{token: token} do
+ conn = build_conn()
+ {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
+ assert {:redirect, %{to: path, flash: flash}} = redirect
+ assert path == ~p"/users/log-in"
+ assert %{"error" => message} = flash
+ assert message == "You must log in to access this page."
+ end
+ end
+end
diff --git a/test/daily_tracker_web/user_auth_test.exs b/test/daily_tracker_web/user_auth_test.exs
@@ -0,0 +1,390 @@
+defmodule DailyTrackerWeb.UserAuthTest do
+ use DailyTrackerWeb.ConnCase, async: true
+
+ alias Phoenix.LiveView
+ alias DailyTracker.Accounts
+ alias DailyTracker.Accounts.Scope
+ alias DailyTrackerWeb.UserAuth
+
+ import DailyTracker.AccountsFixtures
+
+ @remember_me_cookie "_daily_tracker_web_user_remember_me"
+ @remember_me_cookie_max_age 60 * 60 * 24 * 14
+
+ setup %{conn: conn} do
+ conn =
+ conn
+ |> Map.replace!(:secret_key_base, DailyTrackerWeb.Endpoint.config(:secret_key_base))
+ |> init_test_session(%{})
+
+ %{user: %{user_fixture() | authenticated_at: DateTime.utc_now(:second)}, conn: conn}
+ end
+
+ describe "log_in_user/3" do
+ test "stores the user token in the session", %{conn: conn, user: user} do
+ conn = UserAuth.log_in_user(conn, user)
+ assert token = get_session(conn, :user_token)
+ assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
+ assert redirected_to(conn) == ~p"/"
+ assert Accounts.get_user_by_session_token(token)
+ end
+
+ test "clears everything previously stored in the session", %{conn: conn, user: user} do
+ conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
+ refute get_session(conn, :to_be_removed)
+ end
+
+ test "keeps session when re-authenticating", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> assign(:current_scope, Scope.for_user(user))
+ |> put_session(:to_be_removed, "value")
+ |> UserAuth.log_in_user(user)
+
+ assert get_session(conn, :to_be_removed)
+ end
+
+ test "clears session when user does not match when re-authenticating", %{
+ conn: conn,
+ user: user
+ } do
+ other_user = user_fixture()
+
+ conn =
+ conn
+ |> assign(:current_scope, Scope.for_user(other_user))
+ |> put_session(:to_be_removed, "value")
+ |> UserAuth.log_in_user(user)
+
+ refute get_session(conn, :to_be_removed)
+ end
+
+ test "redirects to the configured path", %{conn: conn, user: user} do
+ conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
+ assert redirected_to(conn) == "/hello"
+ end
+
+ test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
+ conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
+ assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
+ assert get_session(conn, :user_remember_me) == true
+
+ assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
+ assert signed_token != get_session(conn, :user_token)
+ assert max_age == @remember_me_cookie_max_age
+ end
+
+ test "redirects to settings when user is already logged in", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> assign(:current_scope, Scope.for_user(user))
+ |> UserAuth.log_in_user(user)
+
+ assert redirected_to(conn) == ~p"/users/settings"
+ end
+
+ test "writes a cookie if remember_me was set in previous session", %{conn: conn, user: user} do
+ conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
+ assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
+ assert get_session(conn, :user_remember_me) == true
+
+ conn =
+ conn
+ |> recycle()
+ |> Map.replace!(:secret_key_base, DailyTrackerWeb.Endpoint.config(:secret_key_base))
+ |> fetch_cookies()
+ |> init_test_session(%{user_remember_me: true})
+
+ # the conn is already logged in and has the remember_me cookie set,
+ # now we log in again and even without explicitly setting remember_me,
+ # the cookie should be set again
+ conn = conn |> UserAuth.log_in_user(user, %{})
+ assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
+ assert signed_token != get_session(conn, :user_token)
+ assert max_age == @remember_me_cookie_max_age
+ assert get_session(conn, :user_remember_me) == true
+ end
+ end
+
+ describe "logout_user/1" do
+ test "erases session and cookies", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+
+ conn =
+ conn
+ |> put_session(:user_token, user_token)
+ |> put_req_cookie(@remember_me_cookie, user_token)
+ |> fetch_cookies()
+ |> UserAuth.log_out_user()
+
+ refute get_session(conn, :user_token)
+ refute conn.cookies[@remember_me_cookie]
+ assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
+ assert redirected_to(conn) == ~p"/"
+ refute Accounts.get_user_by_session_token(user_token)
+ end
+
+ test "broadcasts to the given live_socket_id", %{conn: conn} do
+ live_socket_id = "users_sessions:abcdef-token"
+ DailyTrackerWeb.Endpoint.subscribe(live_socket_id)
+
+ conn
+ |> put_session(:live_socket_id, live_socket_id)
+ |> UserAuth.log_out_user()
+
+ assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
+ end
+
+ test "works even if user is already logged out", %{conn: conn} do
+ conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
+ refute get_session(conn, :user_token)
+ assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
+ assert redirected_to(conn) == ~p"/"
+ end
+ end
+
+ describe "fetch_current_scope_for_user/2" do
+ test "authenticates user from session", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+
+ conn =
+ conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_scope_for_user([])
+
+ assert conn.assigns.current_scope.user.id == user.id
+ assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
+ assert get_session(conn, :user_token) == user_token
+ end
+
+ test "authenticates user from cookies", %{conn: conn, user: user} do
+ logged_in_conn =
+ conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
+
+ user_token = logged_in_conn.cookies[@remember_me_cookie]
+ %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
+
+ conn =
+ conn
+ |> put_req_cookie(@remember_me_cookie, signed_token)
+ |> UserAuth.fetch_current_scope_for_user([])
+
+ assert conn.assigns.current_scope.user.id == user.id
+ assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
+ assert get_session(conn, :user_token) == user_token
+ assert get_session(conn, :user_remember_me)
+
+ assert get_session(conn, :live_socket_id) ==
+ "users_sessions:#{Base.url_encode64(user_token)}"
+ end
+
+ test "does not authenticate if data is missing", %{conn: conn, user: user} do
+ _ = Accounts.generate_user_session_token(user)
+ conn = UserAuth.fetch_current_scope_for_user(conn, [])
+ refute get_session(conn, :user_token)
+ refute conn.assigns.current_scope
+ end
+
+ test "reissues a new token after a few days and refreshes cookie", %{conn: conn, user: user} do
+ logged_in_conn =
+ conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
+
+ token = logged_in_conn.cookies[@remember_me_cookie]
+ %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
+
+ offset_user_token(token, -10, :day)
+ {user, _} = Accounts.get_user_by_session_token(token)
+
+ conn =
+ conn
+ |> put_session(:user_token, token)
+ |> put_session(:user_remember_me, true)
+ |> put_req_cookie(@remember_me_cookie, signed_token)
+ |> UserAuth.fetch_current_scope_for_user([])
+
+ assert conn.assigns.current_scope.user.id == user.id
+ assert conn.assigns.current_scope.user.authenticated_at == user.authenticated_at
+ assert new_token = get_session(conn, :user_token)
+ assert new_token != token
+ assert %{value: new_signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
+ assert new_signed_token != signed_token
+ assert max_age == @remember_me_cookie_max_age
+ end
+ end
+
+ describe "on_mount :mount_current_scope" do
+ setup %{conn: conn} do
+ %{conn: UserAuth.fetch_current_scope_for_user(conn, [])}
+ end
+
+ test "assigns current_scope based on a valid user_token", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_scope.user.id == user.id
+ end
+
+ test "assigns nil to current_scope assign if there isn't a valid user_token", %{conn: conn} do
+ user_token = "invalid_token"
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_scope == nil
+ end
+
+ test "assigns nil to current_scope assign if there isn't a user_token", %{conn: conn} do
+ session = conn |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:mount_current_scope, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_scope == nil
+ end
+ end
+
+ describe "on_mount :require_authenticated" do
+ test "authenticates current_scope based on a valid user_token", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ {:cont, updated_socket} =
+ UserAuth.on_mount(:require_authenticated, %{}, session, %LiveView.Socket{})
+
+ assert updated_socket.assigns.current_scope.user.id == user.id
+ end
+
+ test "redirects to login page if there isn't a valid user_token", %{conn: conn} do
+ user_token = "invalid_token"
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ socket = %LiveView.Socket{
+ endpoint: DailyTrackerWeb.Endpoint,
+ assigns: %{__changed__: %{}, flash: %{}}
+ }
+
+ {:halt, updated_socket} = UserAuth.on_mount(:require_authenticated, %{}, session, socket)
+ assert updated_socket.assigns.current_scope == nil
+ end
+
+ test "redirects to login page if there isn't a user_token", %{conn: conn} do
+ session = conn |> get_session()
+
+ socket = %LiveView.Socket{
+ endpoint: DailyTrackerWeb.Endpoint,
+ assigns: %{__changed__: %{}, flash: %{}}
+ }
+
+ {:halt, updated_socket} = UserAuth.on_mount(:require_authenticated, %{}, session, socket)
+ assert updated_socket.assigns.current_scope == nil
+ end
+ end
+
+ describe "on_mount :require_sudo_mode" do
+ test "allows users that have authenticated in the last 10 minutes", %{conn: conn, user: user} do
+ user_token = Accounts.generate_user_session_token(user)
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ socket = %LiveView.Socket{
+ endpoint: DailyTrackerWeb.Endpoint,
+ assigns: %{__changed__: %{}, flash: %{}}
+ }
+
+ assert {:cont, _updated_socket} =
+ UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
+ end
+
+ test "redirects when authentication is too old", %{conn: conn, user: user} do
+ eleven_minutes_ago = DateTime.utc_now(:second) |> DateTime.add(-11, :minute)
+ user = %{user | authenticated_at: eleven_minutes_ago}
+ user_token = Accounts.generate_user_session_token(user)
+ {user, token_inserted_at} = Accounts.get_user_by_session_token(user_token)
+ assert DateTime.compare(token_inserted_at, user.authenticated_at) == :gt
+ session = conn |> put_session(:user_token, user_token) |> get_session()
+
+ socket = %LiveView.Socket{
+ endpoint: DailyTrackerWeb.Endpoint,
+ assigns: %{__changed__: %{}, flash: %{}}
+ }
+
+ assert {:halt, _updated_socket} =
+ UserAuth.on_mount(:require_sudo_mode, %{}, session, socket)
+ end
+ end
+
+ describe "require_authenticated_user/2" do
+ setup %{conn: conn} do
+ %{conn: UserAuth.fetch_current_scope_for_user(conn, [])}
+ end
+
+ test "redirects if user is not authenticated", %{conn: conn} do
+ conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
+ assert conn.halted
+
+ assert redirected_to(conn) == ~p"/users/log-in"
+
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+ "You must log in to access this page."
+ end
+
+ test "stores the path to redirect to on GET", %{conn: conn} do
+ halted_conn =
+ %{conn | path_info: ["foo"], query_string: ""}
+ |> fetch_flash()
+ |> UserAuth.require_authenticated_user([])
+
+ assert halted_conn.halted
+ assert get_session(halted_conn, :user_return_to) == "/foo"
+
+ halted_conn =
+ %{conn | path_info: ["foo"], query_string: "bar=baz"}
+ |> fetch_flash()
+ |> UserAuth.require_authenticated_user([])
+
+ assert halted_conn.halted
+ assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
+
+ halted_conn =
+ %{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
+ |> fetch_flash()
+ |> UserAuth.require_authenticated_user([])
+
+ assert halted_conn.halted
+ refute get_session(halted_conn, :user_return_to)
+ end
+
+ test "does not redirect if user is authenticated", %{conn: conn, user: user} do
+ conn =
+ conn
+ |> assign(:current_scope, Scope.for_user(user))
+ |> UserAuth.require_authenticated_user([])
+
+ refute conn.halted
+ refute conn.status
+ end
+ end
+
+ describe "disconnect_sessions/1" do
+ test "broadcasts disconnect messages for each token" do
+ tokens = [%{token: "token1"}, %{token: "token2"}]
+
+ for %{token: token} <- tokens do
+ DailyTrackerWeb.Endpoint.subscribe("users_sessions:#{Base.url_encode64(token)}")
+ end
+
+ UserAuth.disconnect_sessions(tokens)
+
+ assert_receive %Phoenix.Socket.Broadcast{
+ event: "disconnect",
+ topic: "users_sessions:dG9rZW4x"
+ }
+
+ assert_receive %Phoenix.Socket.Broadcast{
+ event: "disconnect",
+ topic: "users_sessions:dG9rZW4y"
+ }
+ end
+ end
+end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
@@ -0,0 +1,79 @@
+defmodule DailyTrackerWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use DailyTrackerWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # The default endpoint for testing
+ @endpoint DailyTrackerWeb.Endpoint
+
+ use DailyTrackerWeb, :verified_routes
+
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import DailyTrackerWeb.ConnCase
+ end
+ end
+
+ setup tags do
+ DailyTracker.DataCase.setup_sandbox(tags)
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+
+ @doc """
+ Setup helper that registers and logs in users.
+
+ setup :register_and_log_in_user
+
+ It stores an updated connection and a registered user in the
+ test context.
+ """
+ def register_and_log_in_user(%{conn: conn} = context) do
+ user = DailyTracker.AccountsFixtures.user_fixture()
+ scope = DailyTracker.Accounts.Scope.for_user(user)
+
+ opts =
+ context
+ |> Map.take([:token_authenticated_at])
+ |> Enum.into([])
+
+ %{conn: log_in_user(conn, user, opts), user: user, scope: scope}
+ end
+
+ @doc """
+ Logs the given `user` into the `conn`.
+
+ It returns an updated `conn`.
+ """
+ def log_in_user(conn, user, opts \\ []) do
+ token = DailyTracker.Accounts.generate_user_session_token(user)
+
+ maybe_set_token_authenticated_at(token, opts[:token_authenticated_at])
+
+ conn
+ |> Phoenix.ConnTest.init_test_session(%{})
+ |> Plug.Conn.put_session(:user_token, token)
+ end
+
+ defp maybe_set_token_authenticated_at(_token, nil), do: nil
+
+ defp maybe_set_token_authenticated_at(token, authenticated_at) do
+ DailyTracker.AccountsFixtures.override_token_authenticated_at(token, authenticated_at)
+ end
+end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
@@ -0,0 +1,58 @@
+defmodule DailyTracker.DataCase do
+ @moduledoc """
+ This module defines the setup for tests requiring
+ access to the application's data layer.
+
+ You may define functions here to be used as helpers in
+ your tests.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use DailyTracker.DataCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ alias DailyTracker.Repo
+
+ import Ecto
+ import Ecto.Changeset
+ import Ecto.Query
+ import DailyTracker.DataCase
+ end
+ end
+
+ setup tags do
+ DailyTracker.DataCase.setup_sandbox(tags)
+ :ok
+ end
+
+ @doc """
+ Sets up the sandbox based on the test tags.
+ """
+ def setup_sandbox(tags) do
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(DailyTracker.Repo, shared: not tags[:async])
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+ end
+
+ @doc """
+ A helper that transforms changeset errors into a map of messages.
+
+ assert {:error, changeset} = Accounts.create_user(%{password: "short"})
+ assert "password is too short" in errors_on(changeset).password
+ assert %{password: ["password is too short"]} = errors_on(changeset)
+
+ """
+ def errors_on(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
diff --git a/test/support/fixtures/accounts_fixtures.ex b/test/support/fixtures/accounts_fixtures.ex
@@ -0,0 +1,89 @@
+defmodule DailyTracker.AccountsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `DailyTracker.Accounts` context.
+ """
+
+ import Ecto.Query
+
+ alias DailyTracker.Accounts
+ alias DailyTracker.Accounts.Scope
+
+ def unique_user_email, do: "user#{System.unique_integer()}@example.com"
+ def valid_user_password, do: "hello world!"
+
+ def valid_user_attributes(attrs \\ %{}) do
+ Enum.into(attrs, %{
+ email: unique_user_email()
+ })
+ end
+
+ def unconfirmed_user_fixture(attrs \\ %{}) do
+ {:ok, user} =
+ attrs
+ |> valid_user_attributes()
+ |> Accounts.register_user()
+
+ user
+ end
+
+ def user_fixture(attrs \\ %{}) do
+ user = unconfirmed_user_fixture(attrs)
+
+ token =
+ extract_user_token(fn url ->
+ Accounts.deliver_login_instructions(user, url)
+ end)
+
+ {:ok, {user, _expired_tokens}} =
+ Accounts.login_user_by_magic_link(token)
+
+ user
+ end
+
+ def user_scope_fixture do
+ user = user_fixture()
+ user_scope_fixture(user)
+ end
+
+ def user_scope_fixture(user) do
+ Scope.for_user(user)
+ end
+
+ def set_password(user) do
+ {:ok, {user, _expired_tokens}} =
+ Accounts.update_user_password(user, %{password: valid_user_password()})
+
+ user
+ end
+
+ def extract_user_token(fun) do
+ {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
+ [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
+ token
+ end
+
+ def override_token_authenticated_at(token, authenticated_at) when is_binary(token) do
+ DailyTracker.Repo.update_all(
+ from(t in Accounts.UserToken,
+ where: t.token == ^token
+ ),
+ set: [authenticated_at: authenticated_at]
+ )
+ end
+
+ def generate_user_magic_link_token(user) do
+ {encoded_token, user_token} = Accounts.UserToken.build_email_token(user, "login")
+ DailyTracker.Repo.insert!(user_token)
+ {encoded_token, user_token.token}
+ end
+
+ def offset_user_token(token, amount_to_add, unit) do
+ dt = DateTime.add(DateTime.utc_now(:second), amount_to_add, unit)
+
+ DailyTracker.Repo.update_all(
+ from(ut in Accounts.UserToken, where: ut.token == ^token),
+ set: [inserted_at: dt, authenticated_at: dt]
+ )
+ end
+end
diff --git a/test/support/fixtures/tracker_fixtures.ex b/test/support/fixtures/tracker_fixtures.ex
@@ -0,0 +1,28 @@
+defmodule DailyTracker.TrackerFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `DailyTracker.Tracker` context.
+ """
+
+ @doc """
+ Generate a entry.
+ """
+ def entry_fixture(attrs \\ %{}) do
+ {:ok, entry} =
+ attrs
+ |> Enum.into(%{
+ bowel_movement: true,
+ date: ~D[2026-05-29],
+ headache: true,
+ hunger_level: 42,
+ mood: "some mood",
+ notes: "some notes",
+ period: true,
+ water_glasses: 42,
+ worked_out: true
+ })
+ |> DailyTracker.Tracker.create_entry()
+
+ entry
+ end
+end
diff --git a/test/test_helper.exs b/test/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(DailyTracker.Repo, :manual)