27 | //
28 | plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
29 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
30 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
31 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
32 |
33 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle
34 | // See your `CoreComponents.icon/1` for more information.
35 | //
36 | plugin(function({matchComponents, theme}) {
37 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
38 | let values = {}
39 | let icons = [
40 | ["", "/24/outline"],
41 | ["-solid", "/24/solid"],
42 | ["-mini", "/20/solid"],
43 | ["-micro", "/16/solid"]
44 | ]
45 | icons.forEach(([suffix, dir]) => {
46 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
47 | let name = path.basename(file, ".svg") + suffix
48 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
49 | })
50 | })
51 | matchComponents({
52 | "hero": ({name, fullPath}) => {
53 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
54 | let size = theme("spacing.6")
55 | if (name.endsWith("-mini")) {
56 | size = theme("spacing.5")
57 | } else if (name.endsWith("-micro")) {
58 | size = theme("spacing.4")
59 | }
60 | return {
61 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
62 | "-webkit-mask": `var(--hero-${name})`,
63 | "mask": `var(--hero-${name})`,
64 | "mask-repeat": "no-repeat",
65 | "background-color": "currentColor",
66 | "vertical-align": "middle",
67 | "display": "inline-block",
68 | "width": size,
69 | "height": size
70 | }
71 | }
72 | }, {values})
73 | })
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/native/swiftui/Cookbook/ErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorView.swift
3 | // ErrorView
4 | //
5 |
6 | import SwiftUI
7 | import LiveViewNative
8 |
9 | struct ErrorView: View {
10 | let error: Error
11 |
12 | @Environment(\.reconnectLiveView) private var reconnectLiveView
13 |
14 | var body: some View {
15 | LiveErrorView(error: error) {
16 | if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
17 | ContentUnavailableView {
18 | Label("Connection Failed", systemImage: "network.slash")
19 | } description: {
20 | description
21 | } actions: {
22 | actions
23 | }
24 | } else {
25 | VStack {
26 | Label("Connection Failed", systemImage: "network.slash")
27 | .font(.headline)
28 | description
29 | .foregroundStyle(.secondary)
30 | actions
31 | }
32 | }
33 | }
34 | }
35 |
36 | @ViewBuilder
37 | var description: some View {
38 | #if DEBUG
39 | ScrollView {
40 | Text(error.localizedDescription)
41 | .font(.caption.monospaced())
42 | .multilineTextAlignment(.leading)
43 | }
44 | #else
45 | Text("The app will reconnect when network connection is regained.")
46 | #endif
47 | }
48 |
49 | @ViewBuilder
50 | var actions: some View {
51 | Button {
52 | #if os(iOS)
53 | UIPasteboard.general.string = error.localizedDescription
54 | #elseif os(macOS)
55 | NSPasteboard.general.setString(error.localizedDescription, forType: .string)
56 | #endif
57 | } label: {
58 | Label("Copy Error", systemImage: "doc.on.doc")
59 | }
60 | #if os(watchOS)
61 | SwiftUI.Button {
62 | Task {
63 | await reconnectLiveView(.restart)
64 | }
65 | } label: {
66 | SwiftUI.Label("Restart", systemImage: "arrow.circlepath")
67 | }
68 | .padding()
69 | #else
70 | Menu {
71 | Button {
72 | Task {
73 | await reconnectLiveView(.automatic)
74 | }
75 | } label: {
76 | Label("Reconnect this page", systemImage: "arrow.2.circlepath")
77 | }
78 | Button {
79 | Task {
80 | await reconnectLiveView(.restart)
81 | }
82 | } label: {
83 | Label("Restart from root", systemImage: "arrow.circlepath")
84 | }
85 | } label: {
86 | Label("Reconnect", systemImage: "arrow.2.circlepath")
87 | }
88 | .padding()
89 | #endif
90 | }
91 | }
92 |
93 | #Preview {
94 | ErrorView(error: LiveConnectionError.initialParseError(missingOrInvalid: .csrfToken))
95 | }
96 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
2 | # instead of Alpine to avoid DNS resolution issues in production.
3 | #
4 | # https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
5 | # https://hub.docker.com/_/ubuntu?tab=tags
6 | #
7 | # This file is based on these images:
8 | #
9 | # - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
10 | # - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20240701-slim - for the release image
11 | # - https://pkgs.org/ - resource for finding needed packages
12 | # - Ex: hexpm/elixir:1.17.2-erlang-27.0-debian-bullseye-20240701-slim
13 | #
14 | ARG ELIXIR_VERSION=1.17.2
15 | ARG OTP_VERSION=27.0
16 | ARG DEBIAN_VERSION=bullseye-20240701-slim
17 |
18 | ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
19 | ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
20 |
21 | FROM ${BUILDER_IMAGE} as builder
22 |
23 | # install build dependencies
24 | RUN apt-get update -y && apt-get install -y build-essential git \
25 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
26 |
27 | # prepare build dir
28 | WORKDIR /app
29 |
30 | # install hex + rebar
31 | RUN mix local.hex --force && \
32 | mix local.rebar --force
33 |
34 | # set build ENV
35 | ENV MIX_ENV="prod"
36 |
37 | # install mix dependencies
38 | COPY mix.exs mix.lock ./
39 | RUN mix deps.get --only $MIX_ENV
40 | RUN mkdir config
41 |
42 | # copy compile-time config files before we compile dependencies
43 | # to ensure any relevant config change will trigger the dependencies
44 | # to be re-compiled.
45 | COPY config/config.exs config/${MIX_ENV}.exs config/
46 | RUN mix deps.compile
47 |
48 | COPY priv priv
49 |
50 | COPY lib lib
51 |
52 | COPY assets assets
53 |
54 | # compile assets
55 | RUN mix assets.deploy
56 |
57 | # Compile the release
58 | RUN mix compile
59 |
60 | # Changes to config/runtime.exs don't require recompiling the code
61 | COPY config/runtime.exs config/
62 |
63 | COPY rel rel
64 | RUN mix release
65 |
66 | # start a new build stage so that the final image will only contain
67 | # the compiled release and other runtime necessities
68 | FROM ${RUNNER_IMAGE}
69 |
70 | RUN apt-get update -y && \
71 | apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
72 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
73 |
74 | # Set the locale
75 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
76 |
77 | ENV LANG en_US.UTF-8
78 | ENV LANGUAGE en_US:en
79 | ENV LC_ALL en_US.UTF-8
80 |
81 | WORKDIR "/app"
82 | RUN chown nobody /app
83 |
84 | # set runner ENV
85 | ENV MIX_ENV="prod"
86 |
87 | # Only copy the final release from the build stage
88 | COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/cookbook ./
89 |
90 | USER nobody
91 |
92 | # If using an environment that doesn't automatically reap zombie processes, it is
93 | # advised to add an init process such as tini via `apt-get install`
94 | # above and adding an entrypoint. See https://github.com/krallin/tini for details
95 | # ENTRYPOINT ["/tini", "--"]
96 |
97 | CMD ["/app/bin/server"]
98 |
--------------------------------------------------------------------------------
/lib/cookbook_web/live/cookbook_live.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.CookbookLive do
2 | use CookbookWeb, :live_view
3 | use CookbookNative, :live_view
4 | alias QRCode.Render.SvgSettings
5 |
6 | @featured_recipes ["search", "card-row", "sectioned-grid", "gesture"]
7 |
8 | def handle_params(params, uri, socket) do
9 | uri = URI.parse(uri) |> Map.put(:query, nil) |> URI.to_string()
10 |
11 | {:ok, qr} =
12 | uri
13 | |> QRCode.create(:high)
14 | |> QRCode.render(:svg, %SvgSettings{flatten: true})
15 | |> QRCode.to_base64()
16 |
17 | {:noreply,
18 | socket
19 | |> assign(:qr, qr)
20 | |> assign(:uri, uri)}
21 | end
22 |
23 | def mount(_params, _session, socket) do
24 | all_recipes = recipes(nil)
25 | categorized_recipes = Enum.group_by(all_recipes, & &1.metadata.category)
26 |
27 | categories =
28 | categorized_recipes
29 | |> Map.keys()
30 | |> Enum.sort()
31 |
32 | {:ok,
33 | socket
34 | |> assign(:recipes, all_recipes)
35 | |> assign(:selected_category, nil)
36 | |> assign(:categories, categories)
37 | |> assign(
38 | :featured_recipes,
39 | Enum.filter(all_recipes, &Enum.member?(@featured_recipes, Path.basename(&1.path)))
40 | )}
41 | end
42 |
43 | def handle_event("clear-filter", _params, socket) do
44 | handle_event("filter", %{"category" => nil}, socket)
45 | end
46 |
47 | def handle_event("filter", %{"category" => category}, socket) do
48 | {:noreply,
49 | socket
50 | |> assign(:selected_category, category)
51 | |> assign(:recipes, recipes(category))}
52 | end
53 |
54 | def recipes(nil) do
55 | Phoenix.Router.routes(CookbookWeb.Router)
56 | |> Enum.filter(&String.starts_with?(&1.path, "/recipes/"))
57 | end
58 |
59 | def recipes(category) do
60 | Phoenix.Router.routes(CookbookWeb.Router)
61 | |> Enum.filter(
62 | &(String.starts_with?(&1.path, "/recipes/") and &1.metadata.category == category)
63 | )
64 | end
65 |
66 | def render(assigns) do
67 | ~H"""
68 |
69 |
70 |
See all <%= length(@recipes) %> recipes on your iPhone
71 | <.link
72 | href={"https://appclip.apple.com/id?p=com.dockyard.LiveViewNativeGo.Clip&liveview=#{@uri}"}
73 | class="rounded-lg bg-zinc-900 px-3 py-2 hover:bg-zinc-800/80 text-white"
74 | >
75 | <.icon name="hero-rocket-launch-mini" /> Open in
LVN Go
76 |
77 |
78 |
79 |
Already have LVN Go ?
80 |
81 |
82 | Scan the code with LVN Go to open the cookbook.
83 |
84 |
85 |
86 | """
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/lib/cookbook_web.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, components, channels, and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use CookbookWeb, :controller
9 | use CookbookWeb, :html
10 |
11 | The definitions below will be executed for every controller,
12 | component, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define additional modules and import
17 | those modules here.
18 | """
19 |
20 | def static_paths, do: ~w(
21 | assets
22 | fonts
23 | images
24 | android-chrome-192x192.png
25 | android-chrome-512x512.png
26 | apple-touch-icon.png
27 | favicon-16x16.png
28 | favicon-32x32.png
29 | robots.txt
30 | site.webmanifest
31 | )
32 |
33 | def router do
34 | quote do
35 | use Phoenix.Router, helpers: false
36 |
37 | # Import common connection and controller functions to use in pipelines
38 | import Plug.Conn
39 | import Phoenix.Controller
40 | import Phoenix.LiveView.Router
41 | end
42 | end
43 |
44 | def channel do
45 | quote do
46 | use Phoenix.Channel
47 | end
48 | end
49 |
50 | def controller do
51 | quote do
52 | use Phoenix.Controller,
53 | formats: [:html, :json],
54 | layouts: [html: CookbookWeb.Layouts]
55 |
56 | import Plug.Conn
57 | import CookbookWeb.Gettext
58 |
59 | unquote(verified_routes())
60 | end
61 | end
62 |
63 | def live_view do
64 | quote do
65 | use Phoenix.LiveView,
66 | layout: {CookbookWeb.Layouts, :app}
67 |
68 | unquote(html_helpers())
69 | end
70 | end
71 |
72 | def live_component do
73 | quote do
74 | use Phoenix.LiveComponent
75 |
76 | unquote(html_helpers())
77 | end
78 | end
79 |
80 | def html do
81 | quote do
82 | use Phoenix.Component
83 |
84 | # Import convenience functions from controllers
85 | import Phoenix.Controller,
86 | only: [get_csrf_token: 0, view_module: 1, view_template: 1]
87 |
88 | # Include general helpers for rendering HTML
89 | unquote(html_helpers())
90 | end
91 | end
92 |
93 | defp html_helpers do
94 | quote do
95 | # HTML escaping functionality
96 | import Phoenix.HTML
97 | # Core UI components and translation
98 | import CookbookWeb.CoreComponents
99 | import CookbookWeb.Gettext
100 |
101 | # Shortcut for generating JS commands
102 | alias Phoenix.LiveView.JS
103 |
104 | # Routes generation with the ~p sigil
105 | unquote(verified_routes())
106 | end
107 | end
108 |
109 | def verified_routes do
110 | quote do
111 | use Phoenix.VerifiedRoutes,
112 | endpoint: CookbookWeb.Endpoint,
113 | router: CookbookWeb.Router,
114 | statics: CookbookWeb.static_paths()
115 | end
116 | end
117 |
118 | @doc """
119 | When used, dispatch to the appropriate controller/view/etc.
120 | """
121 | defmacro __using__(which) when is_atom(which) do
122 | apply(__MODULE__, which, [])
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/cookbook_web/live/recipes/pyramid_navigation_live.swiftui.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.PyramidNavigationLive.SwiftUI do
2 | use CookbookNative, [:render_component, format: :swiftui]
3 |
4 | def render(assigns, %{ "target" => "macos" } = _interface) do
5 | ~LVN"""
6 | <%= if @selection != nil do %>
7 |
8 | <.detail_view selection={@selection} target="macos" />
9 |
10 | <% else %>
11 |
12 | <.item_grid />
13 |
14 | <% end %>
15 | """
16 | end
17 |
18 | def render(assigns, _interface) do
19 | ~LVN"""
20 |
27 |
28 | <.detail_view selection={@selection} target="ios" />
29 |
30 |
31 | <.item_grid />
32 |
33 | """
34 | end
35 |
36 | def item_grid(assigns) do
37 | ~LVN"""
38 |
42 | <.button
43 | :for={i <- 1..25}
44 | phx-click="select"
45 | phx-value-selection={i}
46 | >
47 |
54 |
55 |
56 | """
57 | end
58 |
59 | attr :selection, :integer
60 | attr :target, :string
61 | def detail_view(assigns) do
62 | ~LVN"""
63 |
74 |
83 |
84 |
85 |
86 | <.button phx-click="done" style="buttonStyle(.plain);">
87 |
93 |
94 |
95 |
96 |
97 | <.button phx-click="previous" style="padding();">
98 | <.icon name="arrow.left" />
99 |
100 |
101 |
102 | <.button phx-click="next" style="padding();">
103 | <.icon name="arrow.right" />
104 |
105 |
106 |
107 | """
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/config/runtime.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # config/runtime.exs is executed for all environments, including
4 | # during releases. It is executed after compilation and before the
5 | # system starts, so it is typically used to load production configuration
6 | # and secrets from environment variables or elsewhere. Do not define
7 | # any compile-time configuration in here, as it won't be applied.
8 | # The block below contains prod specific runtime configuration.
9 |
10 | # ## Using releases
11 | #
12 | # If you use `mix release`, you need to explicitly enable the server
13 | # by passing the PHX_SERVER=true when you start it:
14 | #
15 | # PHX_SERVER=true bin/cookbook start
16 | #
17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
18 | # script that automatically sets the env var above.
19 | if System.get_env("PHX_SERVER") do
20 | config :cookbook, CookbookWeb.Endpoint, server: true
21 | end
22 |
23 | if config_env() == :prod do
24 | # The secret key base is used to sign/encrypt cookies and other secrets.
25 | # A default value is used in config/dev.exs and config/test.exs but you
26 | # want to use a different value for prod and you most likely don't want
27 | # to check this value into version control, so we use an environment
28 | # variable instead.
29 | secret_key_base =
30 | System.get_env("SECRET_KEY_BASE") ||
31 | raise """
32 | environment variable SECRET_KEY_BASE is missing.
33 | You can generate one by calling: mix phx.gen.secret
34 | """
35 |
36 | host = System.get_env("PHX_HOST") || "example.com"
37 | port = String.to_integer(System.get_env("PORT") || "4000")
38 |
39 | config :cookbook, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
40 |
41 | config :cookbook, CookbookWeb.Endpoint,
42 | url: [host: host, port: 443, scheme: "https"],
43 | http: [
44 | # Enable IPv6 and bind on all interfaces.
45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses.
48 | ip: {0, 0, 0, 0, 0, 0, 0, 0},
49 | port: port
50 | ],
51 | secret_key_base: secret_key_base
52 |
53 | # ## SSL Support
54 | #
55 | # To get SSL working, you will need to add the `https` key
56 | # to your endpoint configuration:
57 | #
58 | # config :cookbook, CookbookWeb.Endpoint,
59 | # https: [
60 | # ...,
61 | # port: 443,
62 | # cipher_suite: :strong,
63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
65 | # ]
66 | #
67 | # The `cipher_suite` is set to `:strong` to support only the
68 | # latest and more secure SSL ciphers. This means old browsers
69 | # and clients may not be supported. You can set it to
70 | # `:compatible` for wider support.
71 | #
72 | # `:keyfile` and `:certfile` expect an absolute path to the key
73 | # and cert in disk or a relative path inside priv, for example
74 | # "priv/ssl/server.key". For all supported SSL configuration
75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
76 | #
77 | # We also recommend setting `force_ssl` in your config/prod.exs,
78 | # ensuring no data is ever sent via http, always redirecting to https:
79 | #
80 | # config :cookbook, CookbookWeb.Endpoint,
81 | # force_ssl: [hsts: true]
82 | #
83 | # Check `Plug.SSL` for all available options in `force_ssl`.
84 | end
85 |
--------------------------------------------------------------------------------
/lib/cookbook_web/live/recipes/media_overview_live.swiftui.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.MediaOverviewLive.SwiftUI do
2 | use CookbookNative, [:render_component, format: :swiftui]
3 |
4 | def render(assigns, _interface) do
5 | ~LVN"""
6 |
7 | <%!-- back button --%>
8 | <%!-- TODO: add action to go back on button press --%>
9 |
10 | <.button style="buttonStyle(.bordereless); tint(.primary);">
11 |
15 |
16 |
17 |
18 | <.button style="buttonStyle(.bordereless); tint(.primary);">
19 |
23 |
24 |
25 | <%!-- cover row --%>
26 |
29 | <%!-- album art space --%>
30 |
31 |
32 |
33 | <%!-- title --%>
34 | Album
35 | Artist
36 | Genre · Year · Metadata
37 |
38 | <%!-- actions --%>
39 |
40 | <.button>
41 | Play
42 |
43 | <.button>
44 | Shuffle
45 |
46 |
47 |
48 | <%!-- description --%>
49 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris magna leo, lacinia ut faucibus quis, sodales eget ex. Aenean et metus euismod, luctus nunc at, lobortis nunc.
50 |
51 | <%!-- background blur --%>
52 |
56 |
60 |
61 |
62 |
63 | <%!-- background image --%>
64 |
65 |
66 |
67 | <%!-- track list --%>
68 | <.button
69 | :for={track <- 1..15}
70 | >
71 |
72 | <%= track %>
73 | Track <%= track %>
74 |
75 |
76 |
77 | """
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/cookbook_web/live/recipes/playback_bar_live.swiftui.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.PlaybackBarLive.SwiftUI do
2 | use CookbookNative, [:render_component, format: :swiftui]
3 |
4 | # iOS
5 |
6 | def render(assigns, %{ "target" => "ios" } = _interface) do
7 | ~LVN"""
8 |
14 | <.song_list />
15 |
16 |
25 | <.expanded_player />
26 |
32 | <.collapsed_player />
33 |
34 |
35 |
36 | """
37 | end
38 |
39 | # macOS
40 |
41 | def render(assigns, %{ "target" => "macos" } = _interface) do
42 | ~LVN"""
43 |
44 | <.song_list />
45 |
46 |
51 | <.collapsed_player />
52 |
53 |
54 |
55 | """
56 | end
57 |
58 | # iPadOS
59 |
60 | def render(assigns, _interface) do
61 | ~LVN"""
62 |
63 | <.song_list />
64 |
65 |
66 | <.collapsed_player />
67 |
68 |
69 | """
70 | end
71 |
72 | ## Components
73 |
74 | def song_list(assigns) do
75 | ~LVN"""
76 |
83 |
84 | Song <%= i %>
85 |
86 |
87 | """
88 | end
89 |
90 | def expanded_player(assigns) do
91 | ~LVN"""
92 |
97 |
105 |
110 |
111 | Song
112 | Artist
113 |
114 |
115 | <.button><.icon name="star.circle.fill" />
116 | <.button><.icon name="ellipsis.circle.fill" />
117 |
118 |
119 | 0:30
120 |
121 |
122 | <.button style="frame(maxWidth: .infinity)"><.icon name="backward.fill" />
123 | <.button style="frame(maxWidth: .infinity); font(.system(size: 60))"><.icon name="pause.fill" />
124 | <.button style="frame(maxWidth: .infinity)"><.icon name="forward.fill" />
125 |
126 |
127 | """
128 | end
129 |
130 | def collapsed_player(assigns) do
131 | ~LVN"""
132 |
133 |
140 | Song
141 |
142 | <.button style="imageScale(.large)"><.icon name="pause.fill" />
143 | <.button><.icon name="forward.fill" />
144 |
145 | """
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/cookbook_web/live/cookbook_live.swiftui.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.CookbookLive.SwiftUI do
2 | use CookbookNative, [:render_component, format: :swiftui]
3 |
4 | def render(assigns, interface) do
5 | target = Map.get(interface, "target", "ios")
6 | assigns = assign(assigns, :target, target)
7 | ~LVN"""
8 |
16 |
17 | <.link href="https://github.com/liveview-native/cookbook" style="buttonStyle(.automatic);">
18 | <.icon name="info.circle" />
19 |
20 |
21 |
22 |
34 |
35 |
41 | <.featured_recipe
42 | :for={{recipe, index} <- Enum.with_index(@featured_recipes)}
43 | recipe={recipe}
44 | hue={index / length(@featured_recipes)}
45 | />
46 |
47 |
48 |
49 |
50 |
51 |
52 | Recipes
53 |
54 |
55 |
56 | <%= @selected_category || "All" %>
57 |
58 |
59 | All
60 |
65 | <%= category %>
66 |
67 |
68 |
69 | <.link
70 | :for={recipe <- @recipes}
71 | navigate={recipe.path}
72 | >
73 |
74 |
75 | <%= recipe.metadata.title %>
76 | <%= recipe.metadata.description %>
77 |
78 |
79 |
80 |
81 |
82 |
83 | """
84 | end
85 |
86 | attr :recipe, :any
87 | attr :hue, :float
88 | def featured_recipe(assigns) do
89 | ~LVN"""
90 | <.link navigate={@recipe.path} style="buttonStyle(.plain);">
91 |
101 | <.icon
102 | name={@recipe.metadata.icon}
103 | style={[
104 | "resizable()",
105 | "symbolRenderingMode(.hierarchical)",
106 | "scaledToFit()",
107 | "padding(10)",
108 | "frame(maxWidth: 130, maxHeight: 130)",
109 | "frame(maxWidth: .infinity, maxHeight: .infinity)",
110 | ]}
111 | />
112 |
113 |
114 | <%= @recipe.metadata.title %>
115 |
116 |
117 | <%= @recipe.metadata.description %>
118 |
119 |
120 |
121 |
122 | """
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/cookbook_native.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookNative do
2 | @moduledoc """
3 | The entrypoint for defining your native interfaces, such
4 | as components, render components, layouts, and live views.
5 |
6 | This can be used in your application as:
7 |
8 | use CookbookNative, :live_view
9 |
10 | The definitions below will be executed for every
11 | component, so keep them short and clean, focused
12 | on imports, uses and aliases.
13 |
14 | Do NOT define functions inside the quoted expressions
15 | below. Instead, define additional modules and import
16 | those modules here.
17 | """
18 |
19 | import CookbookWeb, only: [verified_routes: 0]
20 |
21 | @doc ~S'''
22 | Set up an existing LiveView module for use with LiveView Native
23 |
24 | defmodule MyAppWeb.HomeLive do
25 | use MyAppWeb, :live_view
26 | use MyAppNative, :live_view
27 | end
28 |
29 | An `on_mount` callback will be injected that will negotiate
30 | the inbound connection content type. If it is a LiveView Native
31 | type the `render/1` will be delegated to the format-specific
32 | render component.
33 | '''
34 | def live_view() do
35 | quote do
36 | use LiveViewNative.LiveView,
37 | formats: [
38 | :swiftui
39 | ],
40 | layouts: [
41 | swiftui: {CookbookWeb.Layouts.SwiftUI, :app}
42 | ]
43 |
44 | unquote(verified_routes())
45 | end
46 | end
47 |
48 | @doc ~S'''
49 | Set up a module as a LiveView Native format-specific render component
50 |
51 | defmodule MyAppWeb.HomeLive.SwiftUI do
52 | use MyAppNative, [:render_component, format: :swiftui]
53 |
54 | def render(assigns, _interface) do
55 | ~LVN"""
56 |
Hello, world!
57 | """
58 | end
59 | end
60 | '''
61 | def render_component(opts) do
62 | opts =
63 | opts
64 | |> Keyword.take([:format])
65 | |> Keyword.put(:as, :render)
66 |
67 | quote do
68 | use LiveViewNative.Component, unquote(opts)
69 |
70 | unquote(helpers(opts[:format]))
71 | end
72 | end
73 |
74 | @doc ~S'''
75 | Set up a module as a LiveView Native Component
76 |
77 | defmodule MyAppWeb.Components.CustomSwiftUI do
78 | use MyAppNative, [:component, format: :swiftui]
79 |
80 | attr :msg, :string, :required
81 | def home_textk(assigns) do
82 | ~LVN"""
83 |
@msg
84 | """
85 | end
86 | end
87 |
88 | LiveView Native Components are identical to Phoenix Components. Please
89 | refer to the `Phoenix.Component` documentation for more information.
90 | '''
91 | def component(opts) do
92 | opts = Keyword.take(opts, [:format, :root, :as])
93 |
94 | quote do
95 | use LiveViewNative.Component, unquote(opts)
96 |
97 | unquote(helpers(opts[:format]))
98 | end
99 | end
100 |
101 | @doc ~S'''
102 | Set up a module as a LiveView Natve Layout Component
103 |
104 | defmodule MyAppWeb.Layouts.SwiftUI do
105 | use MyAppNative, [:layout, format: :swiftui]
106 |
107 | embed_templates "layouts_swiftui/*"
108 | end
109 | '''
110 | def layout(opts) do
111 | opts = Keyword.take(opts, [:format, :root])
112 |
113 | quote do
114 | use LiveViewNative.Component, unquote(opts)
115 |
116 | import LiveViewNative.Component, only: [csrf_token: 1]
117 |
118 | unquote(helpers(opts[:format]))
119 | end
120 | end
121 |
122 | defp helpers(format) do
123 | gettext_quoted = quote do
124 | import CookbookWeb.Gettext
125 | end
126 |
127 | plugin = LiveViewNative.fetch_plugin!(format)
128 |
129 | plugin_component_quoted = try do
130 | Code.ensure_compiled!(plugin.component)
131 |
132 | quote do
133 | import unquote(plugin.component)
134 | end
135 | rescue
136 | _ -> nil
137 | end
138 |
139 | live_form_quoted = quote do
140 | import LiveViewNative.LiveForm.Component
141 | end
142 |
143 | core_component_module = Module.concat([CookbookWeb, CoreComponents, plugin.module_suffix])
144 |
145 | core_component_quoted = try do
146 | Code.ensure_compiled!(core_component_module)
147 |
148 | quote do
149 | import unquote(core_component_module)
150 | end
151 | rescue
152 | _ -> nil
153 | end
154 |
155 | [
156 | gettext_quoted,
157 | plugin_component_quoted,
158 | live_form_quoted,
159 | core_component_quoted,
160 | verified_routes()
161 | ]
162 |
163 | end
164 |
165 | @doc """
166 | When used, dispatch to the appropriate controller/view/etc.
167 | """
168 | defmacro __using__([which | opts]) when is_atom(which) do
169 | apply(__MODULE__, which, [opts])
170 | end
171 |
172 | defmacro __using__(which) when is_atom(which) do
173 | apply(__MODULE__, which, [])
174 | end
175 | end
176 |
--------------------------------------------------------------------------------
/lib/cookbook_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.Router do
2 | use CookbookWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, [
6 | "html",
7 | "swiftui"
8 | ]
9 | plug :fetch_session
10 | plug :fetch_live_flash
11 | plug :put_root_layout,
12 | html: {CookbookWeb.Layouts, :root},
13 | swiftui: {CookbookWeb.Layouts.SwiftUI, :root}
14 | plug :protect_from_forgery
15 | plug :put_secure_browser_headers
16 | end
17 |
18 | pipeline :api do
19 | plug :accepts, ["json"]
20 | end
21 |
22 | scope "/", CookbookWeb do
23 | pipe_through :browser
24 |
25 | live "/", CookbookLive
26 |
27 | scope "/recipes" do
28 | live "/card-row", CardRowLive, metadata: %{
29 | title: "Card Row",
30 | icon: "square.stack.fill",
31 | description: "Auto-sized cards that snap when scrolling",
32 | category: "UI"
33 | }
34 | live "/charts", ChartsLive, metadata: %{
35 | title: "Charts",
36 | icon: "chart.xyaxis.line",
37 | description: "Swift Charts addon library",
38 | category: "Addons"
39 | }
40 | live "/drill-down-navigation", DrillDownNavigationLive, metadata: %{
41 | title: "Drill-Down Navigation",
42 | icon: "list.bullet.indent",
43 | description: "Navigation method that navigates to nested pages",
44 | category: "Navigation"
45 | }
46 | live "/gesture", GestureLive, metadata: %{
47 | title: "Gesture",
48 | icon: "hand.draw.fill",
49 | description: "Use `gesture_state` to create fluid interactions",
50 | category: "UI"
51 | }
52 | live "/hub-and-spoke-navigation", HubAndSpokeNavigationLive, metadata: %{
53 | title: "Hub & Spoke Navigation",
54 | icon: "arrow.up.left.and.down.right.and.arrow.up.right.and.down.left",
55 | description: "Navigation method that navigates to and from independent pages",
56 | category: "Navigation"
57 | }
58 | live "/maps", MapsLive, metadata: %{
59 | title: "Maps",
60 | icon: "mappin.and.ellipse",
61 | description: "MapKit addon library",
62 | category: "Addons"
63 | }
64 | live "/media-overview", MediaOverviewLive, metadata: %{
65 | title: "Media Overview",
66 | icon: "play.square.stack",
67 | description: "Apple Music/Podcasts-styled album overview",
68 | category: "UI"
69 | }
70 | live "/message-thread", MessageThreadLive, metadata: %{
71 | title: "Message Thread",
72 | icon: "message.fill",
73 | description: "A list of message bubbles that starts at the bottom",
74 | category: "UI"
75 | }
76 | live "/onboarding", OnboardingLive, metadata: %{
77 | title: "Onboarding",
78 | icon: "list.star",
79 | description: "A list of features available in the app",
80 | category: "UI"
81 | }
82 | live "/playback-bar", PlaybackBarLive, metadata: %{
83 | title: "Playback Bar",
84 | icon: "playpause.fill",
85 | description: "An expandable bar that shows currently playing media",
86 | category: "UI"
87 | }
88 | live "/pyramid-navigation", PyramidNavigationLive, metadata: %{
89 | title: "Pyramid Navigation",
90 | icon: "point.3.filled.connected.trianglepath.dotted",
91 | description: "Navigation method that allows navigation between sibling pages",
92 | category: "Navigation"
93 | }
94 | live "/scroll-automation", ScrollAutomationLive, metadata: %{
95 | title: "Scroll Automation",
96 | icon: "arrow.up.and.down.text.horizontal",
97 | description: "Programatically change the scroll position",
98 | category: "UI"
99 | }
100 | live "/search", SearchLive, metadata: %{
101 | title: "Search",
102 | icon: "magnifyingglass",
103 | description: "A search bar that sends live updates, and detects the submit button.",
104 | category: "UI"
105 | }
106 | live "/sectioned-grid", SectionedGridLive, metadata: %{
107 | title: "Sectioned Grid",
108 | icon: "square.grid.3x3.fill",
109 | description: "A grid of items divided into pinned section headers",
110 | category: "UI"
111 | }
112 | live "/tabs", TabsLive, metadata: %{
113 | title: "Tabs",
114 | icon: "rectangle.split.3x1.fill",
115 | description: "Navigation method that uses a system tab bar",
116 | category: "Navigation"
117 | }
118 | live "/video", VideoLive, metadata: %{
119 | title: "Video",
120 | icon: "play.rectangle.fill",
121 | description: "AVKit addon library for video playback",
122 | category: "Addons"
123 | }
124 | end
125 | end
126 |
127 | # Other scopes may use custom stacks.
128 | # scope "/api", CookbookWeb do
129 | # pipe_through :api
130 | # end
131 |
132 | # Enable LiveDashboard in development
133 | if Application.compile_env(:cookbook, :dev_routes) do
134 | # If you want to use the LiveDashboard in production, you should put
135 | # it behind authentication and allow only admins to access it.
136 | # If your application does not have an admins-only section yet,
137 | # you can use Plug.BasicAuth to set up some basic authentication
138 | # as long as you are also using SSL (which you should anyway).
139 | import Phoenix.LiveDashboard.Router
140 |
141 | scope "/dev" do
142 | pipe_through :browser
143 |
144 | live_dashboard "/dashboard", metrics: CookbookWeb.Telemetry
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/assets/vendor/topbar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license MIT
3 | * topbar 2.0.0, 2023-02-04
4 | * https://buunguyen.github.io/topbar
5 | * Copyright (c) 2021 Buu Nguyen
6 | */
7 | (function (window, document) {
8 | "use strict";
9 |
10 | // https://gist.github.com/paulirish/1579671
11 | (function () {
12 | var lastTime = 0;
13 | var vendors = ["ms", "moz", "webkit", "o"];
14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
15 | window.requestAnimationFrame =
16 | window[vendors[x] + "RequestAnimationFrame"];
17 | window.cancelAnimationFrame =
18 | window[vendors[x] + "CancelAnimationFrame"] ||
19 | window[vendors[x] + "CancelRequestAnimationFrame"];
20 | }
21 | if (!window.requestAnimationFrame)
22 | window.requestAnimationFrame = function (callback, element) {
23 | var currTime = new Date().getTime();
24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
25 | var id = window.setTimeout(function () {
26 | callback(currTime + timeToCall);
27 | }, timeToCall);
28 | lastTime = currTime + timeToCall;
29 | return id;
30 | };
31 | if (!window.cancelAnimationFrame)
32 | window.cancelAnimationFrame = function (id) {
33 | clearTimeout(id);
34 | };
35 | })();
36 |
37 | var canvas,
38 | currentProgress,
39 | showing,
40 | progressTimerId = null,
41 | fadeTimerId = null,
42 | delayTimerId = null,
43 | addEvent = function (elem, type, handler) {
44 | if (elem.addEventListener) elem.addEventListener(type, handler, false);
45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
46 | else elem["on" + type] = handler;
47 | },
48 | options = {
49 | autoRun: true,
50 | barThickness: 3,
51 | barColors: {
52 | 0: "rgba(26, 188, 156, .9)",
53 | ".25": "rgba(52, 152, 219, .9)",
54 | ".50": "rgba(241, 196, 15, .9)",
55 | ".75": "rgba(230, 126, 34, .9)",
56 | "1.0": "rgba(211, 84, 0, .9)",
57 | },
58 | shadowBlur: 10,
59 | shadowColor: "rgba(0, 0, 0, .6)",
60 | className: null,
61 | },
62 | repaint = function () {
63 | canvas.width = window.innerWidth;
64 | canvas.height = options.barThickness * 5; // need space for shadow
65 |
66 | var ctx = canvas.getContext("2d");
67 | ctx.shadowBlur = options.shadowBlur;
68 | ctx.shadowColor = options.shadowColor;
69 |
70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
71 | for (var stop in options.barColors)
72 | lineGradient.addColorStop(stop, options.barColors[stop]);
73 | ctx.lineWidth = options.barThickness;
74 | ctx.beginPath();
75 | ctx.moveTo(0, options.barThickness / 2);
76 | ctx.lineTo(
77 | Math.ceil(currentProgress * canvas.width),
78 | options.barThickness / 2
79 | );
80 | ctx.strokeStyle = lineGradient;
81 | ctx.stroke();
82 | },
83 | createCanvas = function () {
84 | canvas = document.createElement("canvas");
85 | var style = canvas.style;
86 | style.position = "fixed";
87 | style.top = style.left = style.right = style.margin = style.padding = 0;
88 | style.zIndex = 100001;
89 | style.display = "none";
90 | if (options.className) canvas.classList.add(options.className);
91 | document.body.appendChild(canvas);
92 | addEvent(window, "resize", repaint);
93 | },
94 | topbar = {
95 | config: function (opts) {
96 | for (var key in opts)
97 | if (options.hasOwnProperty(key)) options[key] = opts[key];
98 | },
99 | show: function (delay) {
100 | if (showing) return;
101 | if (delay) {
102 | if (delayTimerId) return;
103 | delayTimerId = setTimeout(() => topbar.show(), delay);
104 | } else {
105 | showing = true;
106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
107 | if (!canvas) createCanvas();
108 | canvas.style.opacity = 1;
109 | canvas.style.display = "block";
110 | topbar.progress(0);
111 | if (options.autoRun) {
112 | (function loop() {
113 | progressTimerId = window.requestAnimationFrame(loop);
114 | topbar.progress(
115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
116 | );
117 | })();
118 | }
119 | }
120 | },
121 | progress: function (to) {
122 | if (typeof to === "undefined") return currentProgress;
123 | if (typeof to === "string") {
124 | to =
125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
126 | ? currentProgress
127 | : 0) + parseFloat(to);
128 | }
129 | currentProgress = to > 1 ? 1 : to;
130 | repaint();
131 | return currentProgress;
132 | },
133 | hide: function () {
134 | clearTimeout(delayTimerId);
135 | delayTimerId = null;
136 | if (!showing) return;
137 | showing = false;
138 | if (progressTimerId != null) {
139 | window.cancelAnimationFrame(progressTimerId);
140 | progressTimerId = null;
141 | }
142 | (function loop() {
143 | if (topbar.progress("+.1") >= 1) {
144 | canvas.style.opacity -= 0.05;
145 | if (canvas.style.opacity <= 0.05) {
146 | canvas.style.display = "none";
147 | fadeTimerId = null;
148 | return;
149 | }
150 | }
151 | fadeTimerId = window.requestAnimationFrame(loop);
152 | })();
153 | },
154 | };
155 |
156 | if (typeof module === "object" && typeof module.exports === "object") {
157 | module.exports = topbar;
158 | } else if (typeof define === "function" && define.amd) {
159 | define(function () {
160 | return topbar;
161 | });
162 | } else {
163 | this.topbar = topbar;
164 | }
165 | }.call(this, window, document));
166 |
--------------------------------------------------------------------------------
/lib/cookbook_web/controllers/page_html/home.html.heex:
--------------------------------------------------------------------------------
1 | <.flash_group flash={@flash} />
2 |
3 |
10 |
11 |
15 |
19 |
24 |
29 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
49 |
50 | Phoenix Framework
51 |
52 | v<%= Application.spec(:phoenix, :vsn) %>
53 |
54 |
55 |
56 | Peace of mind from prototype to production.
57 |
58 |
59 | 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.
60 |
61 |
62 |
63 |
133 |
134 |
149 |
164 |
188 |
203 |
218 |
219 |
220 |
221 |
222 |
223 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [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", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
3 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
4 | "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
5 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
6 | "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
7 | "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
8 | "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
9 | "ex_maybe": {:hex, :ex_maybe, "1.1.1", "95c0188191b43bd278e876ae4f0a688922e3ca016a9efd97ee7a0b741a61b899", [:mix], [], "hexpm", "1af8c78c915c7f119a513b300a1702fc5cc9fed42d54fd85995265e4c4b763d2"},
10 | "expo": {:hex, :expo, "1.0.0", "647639267e088717232f4d4451526e7a9de31a3402af7fcbda09b27e9a10395a", [:mix], [], "hexpm", "18d2093d344d97678e8a331ca0391e85d29816f9664a25653fd7e6166827827c"},
11 | "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
12 | "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
13 | "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
14 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
15 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
16 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
17 | "live_view_native": {:hex, :live_view_native, "0.3.0", "a99d773740c7ca0178fdf609e4585dbd9859e1e29f65b67e3301da9afccc0e9c", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.10", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0.4", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.5", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7a343414c210f8f088b4aa5981490a1db8ef39a629ce55f79843919813e92e8c"},
18 | "live_view_native_live_form": {:hex, :live_view_native_live_form, "0.3.0", "71452bd34b065baeacc16bf32ea274a87c8d2489c0fd70346661ff9cd4b8a52a", [:mix], [{:live_view_native, "~> 0.3.0", [hex: :live_view_native, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.4", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "5799d8615197d120b4000f5dd375437223c47f04605be71c7cb41a9e1af5ecb1"},
19 | "live_view_native_stylesheet": {:hex, :live_view_native_stylesheet, "0.3.0", "decd7a02ee490406045649e911426d5ea515543023a29256dca95a409ca9079b", [:mix], [{:live_view_native, "~> 0.3.0", [hex: :live_view_native, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cec8e41b12c5177e1ffd63c061a6eab6455617d53d99c506c3787c8970de7c19"},
20 | "live_view_native_swiftui": {:hex, :live_view_native_swiftui, "0.3.0", "ce6f14c926fd0d6d790eee6195e5e71e32fca15ae11632b5692d885e93e24843", [:mix], [{:live_view_native, "~> 0.3.0", [hex: :live_view_native, repo: "hexpm", optional: false]}, {:makeup_eex, ">= 0.1.1", [hex: :makeup_eex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "be3c1a807445795beb6029f35e4fd6718cc1761db591c7af5fae8f5639c97895"},
21 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
22 | "makeup_eex": {:hex, :makeup_eex, "0.1.2", "93a5ef3d28ed753215dba2d59cb40408b37cccb4a8205e53ef9b5319a992b700", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "6140eafb28215ad7182282fd21d9aa6dcffbfbe0eb876283bc6b768a6c57b0c3"},
23 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
24 | "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"},
25 | "matrix_reloaded": {:hex, :matrix_reloaded, "2.3.0", "eea41bc6713021f8f51dde0c2d6b72e695a99098753baebf0760e10aed8fa777", [:mix], [{:ex_maybe, "~> 1.0", [hex: :ex_maybe, repo: "hexpm", optional: false]}, {:result, "~> 1.7", [hex: :result, repo: "hexpm", optional: false]}], "hexpm", "4013c0cebe5dfffc8f2316675b642fb2f5a1dfc4bdc40d2c0dfa0563358fa496"},
26 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
27 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
28 | "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {: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", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
29 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
30 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [: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", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"},
31 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
32 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [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", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"},
33 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
34 | "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"},
35 | "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
36 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
37 | "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
38 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
39 | "pngex": {:hex, :pngex, "0.1.2", "824c2da291fda236397729f236b29f87b98a434d58124ea9f7fa03d3b3cf8587", [:mix], [], "hexpm", "9f9f2d9aa286d03f6c317017a09e1b548fa0aa6b901291e24dbf65d8212b22b0"},
40 | "qr_code": {:hex, :qr_code, "3.1.0", "102cd634c6ccb0b9b03f8e7866c43f4a839157e3a9a0cc2cfef9dbc81b187ee9", [:mix], [{:ex_maybe, "~> 1.1.1", [hex: :ex_maybe, repo: "hexpm", optional: false]}, {:matrix_reloaded, "~> 2.3", [hex: :matrix_reloaded, repo: "hexpm", optional: false]}, {:pngex, "~> 0.1.0", [hex: :pngex, repo: "hexpm", optional: false]}, {:result, "~> 1.7", [hex: :result, repo: "hexpm", optional: false]}, {:xml_builder, "~> 2.3", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm", "b8b187ae7ac35db52d8ccf6d847c62ce06713e631f051c4460ee387b38365500"},
41 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
42 | "result": {:hex, :result, "1.7.2", "a57c569f7cf5c158d2299d3b5624a48b69bd1520d0771dc711bcf9f3916e8ab6", [:mix], [], "hexpm", "89f98e98cfbf64237ecf4913aa36b76b80463e087775d19953dc4b435a35f087"},
43 | "sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"},
44 | "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
45 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
46 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
47 | "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
48 | "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
49 | "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
50 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
51 | "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [: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", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
52 | "xml_builder": {:hex, :xml_builder, "2.3.0", "69d214c6ad41ae1300b36acff4367551cdfd9dc1b860affc16e103c6b1589053", [:mix], [], "hexpm", "972ec33346a225cd5acd14ab23d4e79042bd37cb904e07e24cd06992dde1a0ed"},
53 | }
54 |
--------------------------------------------------------------------------------
/lib/cookbook_web/components/core_components.swiftui.ex:
--------------------------------------------------------------------------------
1 | defmodule CookbookWeb.CoreComponents.SwiftUI do
2 | @moduledoc """
3 | Provides core UI components built for SwiftUI.
4 |
5 | This file contains feature parity components to your applications's CoreComponent module.
6 | The goal is to retain a common API for fast prototyping. Leveraging your existing knowledge
7 | of the `CookbookWeb.CoreComponents` functions you should expect identical functionality for similarly named
8 | components between web and native. That means utilizing your existing `handle_event/3` functions to manage state
9 | and stay focused on adding new templates for your native applications.
10 |
11 | Icons are referenced by a system name. Read more about the [Xcode Asset Manager](https://developer.apple.com/documentation/xcode/asset-management)
12 | to learn how to include different assets in your LiveView Native applications. In addition, you can also use [SF Symbols](https://developer.apple.com/sf-symbols/).
13 | On any MacOS open Spotlight and search `SF Symbols`. The catalog application will provide a reference name that can be used. All SF Symbols
14 | are incuded with all SwiftUI applications.
15 |
16 | Most of this documentation was "borrowed" from the analog Phoenix generated file to ensure this project is expressing the same behavior.
17 | """
18 |
19 | use LiveViewNative.Component
20 |
21 | import LiveViewNative.LiveForm.Component
22 |
23 | @doc """
24 | Renders an input with label and error messages.
25 |
26 | A `Phoenix.HTML.FormField` may be passed as argument,
27 | which is used to retrieve the input name, id, and values.
28 | Otherwise all attributes may be passed explicitly.
29 |
30 | ## Types
31 |
32 | This function accepts all SwiftUI input types, considering that:
33 |
34 | * You may also set `type="Picker"` to render a `
` tag
35 |
36 | * `type="Toggle"` is used exclusively to render boolean values
37 |
38 | ## Examples
39 |
40 |
41 | <.input field={@form[:email]} type="TextField" />
42 | <.input name="my-input" errors={["oh no!"]} />
43 |
44 |
45 | [INSERT LVATTRDOCS]
46 | """
47 | @doc type: :component
48 |
49 | attr :id, :any, default: nil
50 | attr :name, :any
51 | attr :label, :string, default: nil
52 | attr :value, :any
53 |
54 | attr :type, :string,
55 | default: "TextField",
56 | values: ~w(TextFieldLink DatePicker MultiDatePicker Picker SecureField Slider Stepper TextEditor TextField Toggle hidden)
57 |
58 | attr :field, Phoenix.HTML.FormField,
59 | doc: "a form field struct retrieved from the form, for example: `@form[:email]`"
60 |
61 | attr :errors, :list, default: []
62 | attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
63 | attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
64 | attr :options, :list, doc: "the options to pass to `Phoenix.HTML.Form.options_for_select/2`"
65 | attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
66 |
67 | attr :min, :any, default: nil
68 | attr :max, :any, default: nil
69 |
70 | attr :placeholder, :string, default: nil
71 |
72 | attr :readonly, :boolean, default: false
73 |
74 | attr :autocomplete, :string,
75 | default: "on",
76 | values: ~w(on off)
77 |
78 | attr :rest, :global,
79 | include: ~w(disabled step)
80 |
81 | slot :inner_block
82 |
83 | def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
84 | assigns
85 | |> assign(field: nil, id: assigns.id || field.id)
86 | |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
87 | |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
88 | |> assign_new(:value, fn -> field.value end)
89 | |> assign(
90 | :rest,
91 | Map.put(assigns.rest, :style, [
92 | Map.get(assigns.rest, :style, ""),
93 | (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled(true)", else: ""),
94 | (if assigns.autocomplete == "off", do: "textInputAutocapitalization(.never) autocorrectionDisabled()", else: "")
95 | ] |> Enum.join(" "))
96 | )
97 | |> input()
98 | end
99 |
100 | def input(%{type: "hidden"} = assigns) do
101 | ~LVN"""
102 |
103 | """
104 | end
105 |
106 | def input(%{type: "TextFieldLink"} = assigns) do
107 | ~LVN"""
108 |
109 |
110 | <%= @label %>
111 |
112 | <%= @label %>
113 |
114 |
115 | <.error :for={msg <- @errors}><%= msg %>
116 |
117 | """
118 | end
119 |
120 | def input(%{type: "DatePicker"} = assigns) do
121 | ~LVN"""
122 |
123 |
124 | <%= @label %>
125 |
126 | <.error :for={msg <- @errors}><%= msg %>
127 |
128 | """
129 | end
130 |
131 | def input(%{type: "MultiDatePicker"} = assigns) do
132 | ~LVN"""
133 |
134 |
135 | <%= @label %>
136 | <%= @label %>
137 |
138 | <.error :for={msg <- @errors}><%= msg %>
139 |
140 | """
141 | end
142 |
143 | def input(%{type: "Picker"} = assigns) do
144 | ~LVN"""
145 |
146 |
147 | <%= @label %>
148 |
152 | <%= name %>
153 |
154 |
155 | <.error :for={msg <- @errors}><%= msg %>
156 |
157 | """
158 | end
159 |
160 | def input(%{type: "Slider"} = assigns) do
161 | ~LVN"""
162 |
163 |
164 | <%= @label %>
165 | <%= @label %>
166 |
167 | <.error :for={msg <- @errors}><%= msg %>
168 |
169 | """
170 | end
171 |
172 | def input(%{type: "Stepper"} = assigns) do
173 | ~LVN"""
174 |
175 |
176 | <%= @label %>
177 |
178 |
179 | <.error :for={msg <- @errors}><%= msg %>
180 |
181 | """
182 | end
183 |
184 | def input(%{type: "TextEditor"} = assigns) do
185 | ~LVN"""
186 |
187 |
188 | <%= @label %>
189 |
190 |
191 | <.error :for={msg <- @errors}><%= msg %>
192 |
193 | """
194 | end
195 |
196 | def input(%{type: "TextField"} = assigns) do
197 | ~LVN"""
198 |
199 | <%= @placeholder || @label %>
200 | <.error :for={msg <- @errors}><%= msg %>
201 |
202 | """
203 | end
204 |
205 | def input(%{type: "SecureField"} = assigns) do
206 | ~LVN"""
207 |
208 | <%= @placeholder || @label %>
209 | <.error :for={msg <- @errors}><%= msg %>
210 |
211 | """
212 | end
213 |
214 | def input(%{type: "Toggle"} = assigns) do
215 | ~LVN"""
216 |
217 |
218 | <%= @label %>
219 |
220 |
221 | <.error :for={msg <- @errors}><%= msg %>
222 |
223 | """
224 | end
225 |
226 | @doc """
227 | Generates a generic error message.
228 | """
229 | @doc type: :component
230 | slot :inner_block, required: true
231 |
232 | def error(assigns) do
233 | ~LVN"""
234 |
235 | <%= render_slot(@inner_block) %>
236 |
237 | """
238 | end
239 |
240 | @doc """
241 | Renders a header with title.
242 |
243 | [INSERT LVATTRDOCS]
244 | """
245 | @doc type: :component
246 |
247 | slot :inner_block, required: true
248 | slot :subtitle
249 | slot :actions
250 |
251 | def header(assigns) do
252 | ~LVN"""
253 |
258 |
259 | <%= render_slot(@inner_block) %>
260 |
261 |
262 | <%= render_slot(@subtitle) %>
263 |
264 |
265 | <%= render_slot(@actions) %>
266 |
267 |
268 | """
269 | end
270 |
271 | @doc """
272 | Renders a modal.
273 |
274 | ## Examples
275 |
276 | <.modal show={@show} id="confirm-modal">
277 | This is a modal.
278 |
279 |
280 | An event name may be passed to the `:on_cancel` to configure
281 | the closing/cancel event, for example:
282 |
283 | <.modal show={@show} id="confirm" on_cancel="toggle-show">
284 | This is another modal.
285 |
286 |
287 | """
288 | attr :id, :string, required: true
289 | attr :show, :boolean, default: false
290 | attr :on_cancel, :string, default: nil
291 | slot :inner_block, required: true
292 |
293 | def modal(assigns) do
294 | ~LVN"""
295 |
302 |
303 | <%= render_slot(@inner_block) %>
304 |
305 |
306 | """
307 | end
308 |
309 | @doc """
310 | Renders flash notices.
311 |
312 | ## Examples
313 |
314 | <.flash kind={:info} flash={@flash} />
315 | <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
316 | """
317 | attr :id, :string, doc: "the optional id of flash container"
318 | attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
319 | attr :title, :string, default: nil
320 | attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
321 | attr :rest, :global, doc: "the arbitrary attributes to add to the flash container"
322 |
323 | slot :inner_block, doc: "the optional inner block that renders the flash message"
324 |
325 | def flash(assigns) do
326 | assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
327 |
328 | ~LVN"""
329 | <% msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind) %>
330 |
341 | <%= msg %>
342 | OK
343 |
344 | """
345 | end
346 |
347 | @doc """
348 | Shows the flash group with standard titles and content.
349 |
350 | ## Examples
351 |
352 | <.flash_group flash={@flash} />
353 | """
354 | attr :flash, :map, required: true, doc: "the map of flash messages"
355 | attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
356 |
357 | def flash_group(assigns) do
358 | ~LVN"""
359 |
360 | <.flash kind={:info} title={"Success!"} flash={@flash} />
361 | <.flash kind={:error} title={"Error!"} flash={@flash} />
362 |
363 | """
364 | end
365 |
366 | @doc """
367 | Renders a simple form.
368 |
369 | ## Examples
370 |
371 | <.simple_form for={@form} phx-change="validate" phx-submit="save">
372 | <.input field={@form[:email]} label="Email"/>
373 | <.input field={@form[:username]} label="Username" />
374 | <:actions>
375 | <.button type="submit">Save
376 |
377 |
378 |
379 | [INSERT LVATTRDOCS]
380 | """
381 | @doc type: :component
382 |
383 | attr :for, :any, required: true, doc: "the datastructure for the form"
384 | attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
385 |
386 | attr :rest, :global,
387 | include: ~w(autocomplete name rel action enctype method novalidate target multipart),
388 | doc: "the arbitrary attributes to apply to the form tag"
389 |
390 | slot :inner_block, required: true
391 | slot :actions, doc: "the slot for form actions, such as a submit button"
392 |
393 | def simple_form(assigns) do
394 | ~LVN"""
395 | <.form :let={f} for={@for} as={@as} {@rest}>
396 |
404 |
405 | """
406 | end
407 |
408 | @doc """
409 | Renders a button.
410 |
411 | ## Examples
412 |
413 | <.button type="submit">Send!
414 | <.button phx-click="go">Send!
415 | """
416 | @doc type: :component
417 |
418 | attr :type, :string, default: nil
419 | attr :rest, :global
420 |
421 | slot :inner_block, required: true
422 |
423 | def button(%{ type: "submit" } = assigns) do
424 | ~LVN"""
425 |
426 |
432 |
436 | <%= render_slot(@inner_block) %>
437 |
438 |
439 |
440 | """
441 | end
442 |
443 | def button(assigns) do
444 | ~LVN"""
445 |
446 | <%= render_slot(@inner_block) %>
447 |
448 | """
449 | end
450 |
451 | @doc ~S"""
452 | Renders a table with generic styling.
453 |
454 | ## Examples
455 |
456 | <.table id="users" rows={@users}>
457 | <:col :let={user} label="id"><%= user.id %>
458 | <:col :let={user} label="username"><%= user.username %>
459 |
460 | """
461 | @doc type: :component
462 |
463 | attr :id, :string, required: true
464 | attr :rows, :list, required: true
465 | attr :row_id, :any, default: nil, doc: "the function for generating the row id"
466 |
467 | attr :row_item, :any,
468 | default: &Function.identity/1,
469 | doc: "the function for mapping each row before calling the :col and :action slots"
470 |
471 | slot :col, required: true do
472 | attr :label, :string
473 | end
474 |
475 | slot :action, doc: "the slot for showing user actions in the last table column"
476 |
477 | def table(assigns) do
478 | ~LVN"""
479 |
480 |
481 | <%= col[:label] %>
482 |
483 |
484 |
485 |
489 |
490 | <%= render_slot(col, @row_item.(row)) %>
491 |
492 |
493 | <%= for action <- @action do %>
494 | <%= render_slot(action, @row_item.(row)) %>
495 | <% end %>
496 |
497 |
498 |
499 |
500 | """
501 | end
502 |
503 | @doc """
504 | Renders a data list.
505 |
506 | ## Examples
507 |
508 | <.list>
509 | <:item title="Title"><%= @post.title %>
510 | <:item title="Views"><%= @post.views %>
511 |
512 | """
513 | slot :item, required: true do
514 | attr :title, :string, required: true
515 | end
516 |
517 | def list(assigns) do
518 | ~LVN"""
519 |
520 |
521 | <%= item.title %>
522 | <%= render_slot(item) %>
523 |
524 |
525 | """
526 | end
527 |
528 | @doc """
529 | Renders a system image from the Asset Manager in Xcode
530 | or from SF Symbols.
531 |
532 | ## Examples
533 |
534 | <.icon name="xmark.diamond" />
535 | """
536 | @doc type: :component
537 |
538 | attr :name, :string, required: true
539 | attr :rest, :global
540 |
541 | def icon(assigns) do
542 | ~LVN"""
543 |
544 | """
545 | end
546 |
547 | @doc """
548 | Renders an image from a url
549 |
550 | Will render an [`AsyncImage`](https://developer.apple.com/documentation/swiftui/asyncimage)
551 | You can customize the lifecycle states of with the slots.
552 | """
553 |
554 | attr :url, :string, required: true
555 | attr :rest, :global
556 | slot :empty, doc: """
557 | The empty state that will render before has successfully been downloaded.
558 |
559 | <.image url={~p"/assets/images/logo.png"}>
560 | <:empty>
561 |
562 |
563 |
564 |
565 | [See SwiftUI docs](https://developer.apple.com/documentation/swiftui/asyncimagephase/success(_:))
566 | """
567 | slot :success, doc: """
568 | The success state that will render when the image has successfully been downloaded.
569 |
570 | <.image url={~p"/assets/images/logo.png"}>
571 | <:success class="main-logo"/>
572 |
573 |
574 | [See SwiftUI docs](https://developer.apple.com/documentation/swiftui/asyncimagephase/success(_:))
575 | """
576 | do
577 | attr :class, :string
578 | attr :style, :string
579 | end
580 | slot :failure, doc: """
581 | The failure state that will render when the image fails to downloaded.
582 |
583 | <.image url={~p"/assets/images/logo.png"}>
584 | <:failure class="image-fail"/>
585 |
586 |
587 | [See SwiftUI docs](https://developer.apple.com/documentation/swiftui/asyncimagephase/failure(_:))
588 |
589 | """
590 | do
591 | attr :class, :string
592 | attr :style, :string
593 | end
594 |
595 | def image(assigns) do
596 | ~LVN"""
597 |
598 |
599 | <%= render_slot(@empty) %>
600 |
601 | <.image_success slot={@success} />
602 | <.image_failure slot={@failure} />
603 |
604 | """
605 | end
606 |
607 | defp image_success(%{ slot: [%{ inner_block: nil }] } = assigns) do
608 | ~LVN"""
609 |
610 | """
611 | end
612 |
613 | defp image_success(assigns) do
614 | ~LVN"""
615 |
616 | <%= render_slot(@slot) %>
617 |
618 | """
619 | end
620 |
621 | defp image_failure(%{ slot: [%{ inner_block: nil }] } = assigns) do
622 | ~LVN"""
623 |
624 | """
625 | end
626 |
627 | defp image_failure(assigns) do
628 | ~LVN"""
629 |
630 | <%= render_slot(@slot) %>
631 |
632 | """
633 | end
634 |
635 | @doc """
636 | Translates an error message using gettext.
637 | """
638 | def translate_error({msg, opts}) do
639 | # When using gettext, we typically pass the strings we want
640 | # to translate as a static argument:
641 | #
642 | # # Translate the number of files with plural rules
643 | # dngettext("errors", "1 file", "%{count} files", count)
644 | #
645 | # However the error messages in our forms and APIs are generated
646 | # dynamically, so we need to translate them by calling Gettext
647 | # with our gettext backend as first argument. Translations are
648 | # available in the errors.po file (as we use the "errors" domain).
649 | if count = opts[:count] do
650 | Gettext.dngettext(CookbookWeb.Gettext, "errors", msg, msg, count, opts)
651 | else
652 | Gettext.dgettext(CookbookWeb.Gettext, "errors", msg, opts)
653 | end
654 | end
655 |
656 | @doc """
657 | Translates the errors for a field from a keyword list of errors.
658 | """
659 | def translate_errors(errors, field) when is_list(errors) do
660 | for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
661 | end
662 | end
663 |
--------------------------------------------------------------------------------
/native/swiftui/Cookbook.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 3AB690BC90D3D07EBC5754B4 /* Cookbook.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73351403763FF7723C258B5 /* Cookbook.swift */; };
11 | 40FCF15057D489D079D546CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B6FDD79A78563E54B27F4A8 /* Assets.xcassets */; };
12 | 49E816E535CBF8F84AC51B50 /* DisconnectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE17ADA059E77778A3DA0BD /* DisconnectedView.swift */; };
13 | 5A197DC3B4170B841590E2B2 /* LiveViewNative in Frameworks */ = {isa = PBXBuildFile; productRef = E561105E77C821C6381C5DBB /* LiveViewNative */; };
14 | 5B545FFA398FF94EE687A7E1 /* ReconnectingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E2186DDE49BF160C66946AD /* ReconnectingView.swift */; };
15 | 646E1AEB88911A014003BC8A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4D2B420B69B7763058A2A22B /* Preview Assets.xcassets */; };
16 | 6ACE9A1207FF26A2ABBBC330 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4381A4CA492A6A8EE2BCF376 /* ErrorView.swift */; };
17 | 6AF7B78B3194B618296C92DF /* LiveViewNativeLiveForm in Frameworks */ = {isa = PBXBuildFile; productRef = C565AB1EDA23B71D79160D52 /* LiveViewNativeLiveForm */; };
18 | 703EB1AF426AA33E99A3F322 /* ReconnectingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E2186DDE49BF160C66946AD /* ReconnectingView.swift */; };
19 | 781DA8631BB43E25658A5A37 /* LiveViewNativeLiveForm in Frameworks */ = {isa = PBXBuildFile; productRef = F5E1355548AE8C606CACAB0F /* LiveViewNativeLiveForm */; };
20 | 783EF2113BAE8902925AA123 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B6FDD79A78563E54B27F4A8 /* Assets.xcassets */; };
21 | 78CA8249E7DDB6C99CCB0D18 /* Cookbook.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73351403763FF7723C258B5 /* Cookbook.swift */; };
22 | 9EF3A526A2DA98859193A07D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9577C4B68640C5E2DA5DA8B /* ContentView.swift */; };
23 | A1930A7CE3103773182551A2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4D2B420B69B7763058A2A22B /* Preview Assets.xcassets */; };
24 | B48F22CA9D9C35D2D6D0F1C0 /* ConnectingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396096AAE3B95497E52128D /* ConnectingView.swift */; };
25 | B75727C8EEF1EF8DC9E392A3 /* ConnectingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396096AAE3B95497E52128D /* ConnectingView.swift */; };
26 | E08213E66F1869C1E0D2A0B4 /* LiveViewNative in Frameworks */ = {isa = PBXBuildFile; productRef = AB5B13C357FA007CBAE59E65 /* LiveViewNative */; };
27 | E4C63108A5FBE15C1B23A183 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9577C4B68640C5E2DA5DA8B /* ContentView.swift */; };
28 | F2871BB09E726282E1875355 /* DisconnectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE17ADA059E77778A3DA0BD /* DisconnectedView.swift */; };
29 | F558271B162F41C9F95D99FA /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4381A4CA492A6A8EE2BCF376 /* ErrorView.swift */; };
30 | /* End PBXBuildFile section */
31 |
32 | /* Begin PBXFileReference section */
33 | 0396096AAE3B95497E52128D /* ConnectingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectingView.swift; sourceTree = ""; };
34 | 1B6FDD79A78563E54B27F4A8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
35 | 2CE17ADA059E77778A3DA0BD /* DisconnectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectedView.swift; sourceTree = ""; };
36 | 4381A4CA492A6A8EE2BCF376 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; };
37 | 4D2B420B69B7763058A2A22B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
38 | 543712B9F9A2C3AB653F94F4 /* Cookbook.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Cookbook.app; sourceTree = BUILT_PRODUCTS_DIR; };
39 | 6BF528F2A6C00C49532DED21 /* Cookbook Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Cookbook Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
40 | 7E2186DDE49BF160C66946AD /* ReconnectingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReconnectingView.swift; sourceTree = ""; };
41 | E73351403763FF7723C258B5 /* Cookbook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cookbook.swift; sourceTree = ""; };
42 | E9577C4B68640C5E2DA5DA8B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
43 | /* End PBXFileReference section */
44 |
45 | /* Begin PBXFrameworksBuildPhase section */
46 | 2EAB2F36F6EF6C8513905178 /* Frameworks */ = {
47 | isa = PBXFrameworksBuildPhase;
48 | buildActionMask = 2147483647;
49 | files = (
50 | E08213E66F1869C1E0D2A0B4 /* LiveViewNative in Frameworks */,
51 | 6AF7B78B3194B618296C92DF /* LiveViewNativeLiveForm in Frameworks */,
52 | );
53 | runOnlyForDeploymentPostprocessing = 0;
54 | };
55 | 8668C635CB837AB48BA49809 /* Frameworks */ = {
56 | isa = PBXFrameworksBuildPhase;
57 | buildActionMask = 2147483647;
58 | files = (
59 | 5A197DC3B4170B841590E2B2 /* LiveViewNative in Frameworks */,
60 | 781DA8631BB43E25658A5A37 /* LiveViewNativeLiveForm in Frameworks */,
61 | );
62 | runOnlyForDeploymentPostprocessing = 0;
63 | };
64 | /* End PBXFrameworksBuildPhase section */
65 |
66 | /* Begin PBXGroup section */
67 | 1B3D9F50EF824308A6B5B4C1 = {
68 | isa = PBXGroup;
69 | children = (
70 | AFE5673A269B02F31FBFA938 /* Cookbook */,
71 | 648F02669203114763E49687 /* Products */,
72 | );
73 | sourceTree = "";
74 | };
75 | 5D0EBEA340912DC691952C37 /* Preview Content */ = {
76 | isa = PBXGroup;
77 | children = (
78 | 4D2B420B69B7763058A2A22B /* Preview Assets.xcassets */,
79 | );
80 | path = "Preview Content";
81 | sourceTree = "";
82 | };
83 | 648F02669203114763E49687 /* Products */ = {
84 | isa = PBXGroup;
85 | children = (
86 | 6BF528F2A6C00C49532DED21 /* Cookbook Watch App.app */,
87 | 543712B9F9A2C3AB653F94F4 /* Cookbook.app */,
88 | );
89 | name = Products;
90 | sourceTree = "";
91 | };
92 | AFE5673A269B02F31FBFA938 /* Cookbook */ = {
93 | isa = PBXGroup;
94 | children = (
95 | 5D0EBEA340912DC691952C37 /* Preview Content */,
96 | 1B6FDD79A78563E54B27F4A8 /* Assets.xcassets */,
97 | 0396096AAE3B95497E52128D /* ConnectingView.swift */,
98 | E9577C4B68640C5E2DA5DA8B /* ContentView.swift */,
99 | E73351403763FF7723C258B5 /* Cookbook.swift */,
100 | 2CE17ADA059E77778A3DA0BD /* DisconnectedView.swift */,
101 | 4381A4CA492A6A8EE2BCF376 /* ErrorView.swift */,
102 | 7E2186DDE49BF160C66946AD /* ReconnectingView.swift */,
103 | );
104 | path = Cookbook;
105 | sourceTree = "";
106 | };
107 | /* End PBXGroup section */
108 |
109 | /* Begin PBXNativeTarget section */
110 | 096878449CD4A1C6C83621C8 /* Cookbook */ = {
111 | isa = PBXNativeTarget;
112 | buildConfigurationList = 0E51192C320522B946C83A31 /* Build configuration list for PBXNativeTarget "Cookbook" */;
113 | buildPhases = (
114 | E8FF413DBC93932C8B6AB1CF /* Sources */,
115 | 1B43A9C2C66D21A06489C51B /* Resources */,
116 | 2EAB2F36F6EF6C8513905178 /* Frameworks */,
117 | );
118 | buildRules = (
119 | );
120 | dependencies = (
121 | );
122 | name = Cookbook;
123 | packageProductDependencies = (
124 | AB5B13C357FA007CBAE59E65 /* LiveViewNative */,
125 | C565AB1EDA23B71D79160D52 /* LiveViewNativeLiveForm */,
126 | );
127 | productName = Cookbook;
128 | productReference = 543712B9F9A2C3AB653F94F4 /* Cookbook.app */;
129 | productType = "com.apple.product-type.application";
130 | };
131 | 57DE252CB3ACAD4018B31ED6 /* Cookbook Watch App */ = {
132 | isa = PBXNativeTarget;
133 | buildConfigurationList = 1301D702767EC0CFA9F824AE /* Build configuration list for PBXNativeTarget "Cookbook Watch App" */;
134 | buildPhases = (
135 | 345641EFD5E0D0D9F0523C6A /* Sources */,
136 | 858B1BA0126888D1E7E37155 /* Resources */,
137 | 8668C635CB837AB48BA49809 /* Frameworks */,
138 | );
139 | buildRules = (
140 | );
141 | dependencies = (
142 | );
143 | name = "Cookbook Watch App";
144 | packageProductDependencies = (
145 | E561105E77C821C6381C5DBB /* LiveViewNative */,
146 | F5E1355548AE8C606CACAB0F /* LiveViewNativeLiveForm */,
147 | );
148 | productName = "Cookbook Watch App";
149 | productReference = 6BF528F2A6C00C49532DED21 /* Cookbook Watch App.app */;
150 | productType = "com.apple.product-type.application";
151 | };
152 | /* End PBXNativeTarget section */
153 |
154 | /* Begin PBXProject section */
155 | 79F256F62D4F49097CE189BE /* Project object */ = {
156 | isa = PBXProject;
157 | attributes = {
158 | BuildIndependentTargetsInParallel = YES;
159 | LastUpgradeCheck = 1430;
160 | TargetAttributes = {
161 | };
162 | };
163 | buildConfigurationList = B6AF13F86B2F6897FD1FA06C /* Build configuration list for PBXProject "Cookbook" */;
164 | compatibilityVersion = "Xcode 14.0";
165 | developmentRegion = en;
166 | hasScannedForEncodings = 0;
167 | knownRegions = (
168 | Base,
169 | en,
170 | );
171 | mainGroup = 1B3D9F50EF824308A6B5B4C1;
172 | packageReferences = (
173 | 611005AF0B5265A88B338909 /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */,
174 | 8324F855FA52F95C40E26516 /* XCRemoteSwiftPackageReference "liveview-native-live-form" */,
175 | );
176 | projectDirPath = "";
177 | projectRoot = "";
178 | targets = (
179 | 096878449CD4A1C6C83621C8 /* Cookbook */,
180 | 57DE252CB3ACAD4018B31ED6 /* Cookbook Watch App */,
181 | );
182 | };
183 | /* End PBXProject section */
184 |
185 | /* Begin PBXResourcesBuildPhase section */
186 | 1B43A9C2C66D21A06489C51B /* Resources */ = {
187 | isa = PBXResourcesBuildPhase;
188 | buildActionMask = 2147483647;
189 | files = (
190 | 783EF2113BAE8902925AA123 /* Assets.xcassets in Resources */,
191 | A1930A7CE3103773182551A2 /* Preview Assets.xcassets in Resources */,
192 | );
193 | runOnlyForDeploymentPostprocessing = 0;
194 | };
195 | 858B1BA0126888D1E7E37155 /* Resources */ = {
196 | isa = PBXResourcesBuildPhase;
197 | buildActionMask = 2147483647;
198 | files = (
199 | 40FCF15057D489D079D546CF /* Assets.xcassets in Resources */,
200 | 646E1AEB88911A014003BC8A /* Preview Assets.xcassets in Resources */,
201 | );
202 | runOnlyForDeploymentPostprocessing = 0;
203 | };
204 | /* End PBXResourcesBuildPhase section */
205 |
206 | /* Begin PBXSourcesBuildPhase section */
207 | 345641EFD5E0D0D9F0523C6A /* Sources */ = {
208 | isa = PBXSourcesBuildPhase;
209 | buildActionMask = 2147483647;
210 | files = (
211 | B48F22CA9D9C35D2D6D0F1C0 /* ConnectingView.swift in Sources */,
212 | 9EF3A526A2DA98859193A07D /* ContentView.swift in Sources */,
213 | 78CA8249E7DDB6C99CCB0D18 /* Cookbook.swift in Sources */,
214 | 49E816E535CBF8F84AC51B50 /* DisconnectedView.swift in Sources */,
215 | 6ACE9A1207FF26A2ABBBC330 /* ErrorView.swift in Sources */,
216 | 703EB1AF426AA33E99A3F322 /* ReconnectingView.swift in Sources */,
217 | );
218 | runOnlyForDeploymentPostprocessing = 0;
219 | };
220 | E8FF413DBC93932C8B6AB1CF /* Sources */ = {
221 | isa = PBXSourcesBuildPhase;
222 | buildActionMask = 2147483647;
223 | files = (
224 | B75727C8EEF1EF8DC9E392A3 /* ConnectingView.swift in Sources */,
225 | E4C63108A5FBE15C1B23A183 /* ContentView.swift in Sources */,
226 | 3AB690BC90D3D07EBC5754B4 /* Cookbook.swift in Sources */,
227 | F2871BB09E726282E1875355 /* DisconnectedView.swift in Sources */,
228 | F558271B162F41C9F95D99FA /* ErrorView.swift in Sources */,
229 | 5B545FFA398FF94EE687A7E1 /* ReconnectingView.swift in Sources */,
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | /* End PBXSourcesBuildPhase section */
234 |
235 | /* Begin XCBuildConfiguration section */
236 | 0E5C3BAAE29CE190C2144FD1 /* Release */ = {
237 | isa = XCBuildConfiguration;
238 | buildSettings = {
239 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
240 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Cookbook.watchkit;
241 | SDKROOT = watchos;
242 | SKIP_INSTALL = YES;
243 | TARGETED_DEVICE_FAMILY = 4;
244 | };
245 | name = Release;
246 | };
247 | 4AE5AAFEAACDDC047FEA1360 /* Debug */ = {
248 | isa = XCBuildConfiguration;
249 | buildSettings = {
250 | ALWAYS_SEARCH_USER_PATHS = NO;
251 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
252 | CLANG_ANALYZER_NONNULL = YES;
253 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
254 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
255 | CLANG_CXX_LIBRARY = "libc++";
256 | CLANG_ENABLE_MODULES = YES;
257 | CLANG_ENABLE_OBJC_ARC = YES;
258 | CLANG_ENABLE_OBJC_WEAK = YES;
259 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
260 | CLANG_WARN_BOOL_CONVERSION = YES;
261 | CLANG_WARN_COMMA = YES;
262 | CLANG_WARN_CONSTANT_CONVERSION = YES;
263 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
264 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
265 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
266 | CLANG_WARN_EMPTY_BODY = YES;
267 | CLANG_WARN_ENUM_CONVERSION = YES;
268 | CLANG_WARN_INFINITE_RECURSION = YES;
269 | CLANG_WARN_INT_CONVERSION = YES;
270 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
271 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
272 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
273 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
274 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
275 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
276 | CLANG_WARN_STRICT_PROTOTYPES = YES;
277 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
278 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
279 | CLANG_WARN_UNREACHABLE_CODE = YES;
280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
281 | COPY_PHASE_STRIP = NO;
282 | CURRENT_PROJECT_VERSION = 1.0;
283 | DEAD_CODE_STRIPPING = YES;
284 | DEBUG_INFORMATION_FORMAT = dwarf;
285 | ENABLE_STRICT_OBJC_MSGSEND = YES;
286 | ENABLE_TESTABILITY = YES;
287 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
288 | GCC_C_LANGUAGE_STANDARD = gnu11;
289 | GCC_DYNAMIC_NO_PIC = NO;
290 | GCC_NO_COMMON_BLOCKS = YES;
291 | GCC_OPTIMIZATION_LEVEL = 0;
292 | GCC_PREPROCESSOR_DEFINITIONS = (
293 | "$(inherited)",
294 | "DEBUG=1",
295 | );
296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
298 | GCC_WARN_UNDECLARED_SELECTOR = YES;
299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
300 | GCC_WARN_UNUSED_FUNCTION = YES;
301 | GCC_WARN_UNUSED_VARIABLE = YES;
302 | GENERATE_INFOPLIST_FILE = YES;
303 | INFOPLIST_KEY_CFBundleDisplayName = Cookbook;
304 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
305 | INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.example.Cookbook;
306 | INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES;
307 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
308 | MACOSX_DEPLOYMENT_TARGET = 13.0;
309 | MARKETING_VERSION = 1.0;
310 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
311 | MTL_FAST_MATH = YES;
312 | ONLY_ACTIVE_ARCH = YES;
313 | PRODUCT_NAME = "$(TARGET_NAME)";
314 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
315 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
316 | SWIFT_VERSION = 5.0;
317 | TVOS_DEPLOYMENT_TARGET = 16.0;
318 | WATCHOS_DEPLOYMENT_TARGET = 9.0;
319 | };
320 | name = Debug;
321 | };
322 | 76E09144664DF93C52F8B394 /* Release */ = {
323 | isa = XCBuildConfiguration;
324 | buildSettings = {
325 | ALWAYS_SEARCH_USER_PATHS = NO;
326 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
327 | CLANG_ANALYZER_NONNULL = YES;
328 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
330 | CLANG_CXX_LIBRARY = "libc++";
331 | CLANG_ENABLE_MODULES = YES;
332 | CLANG_ENABLE_OBJC_ARC = YES;
333 | CLANG_ENABLE_OBJC_WEAK = YES;
334 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
335 | CLANG_WARN_BOOL_CONVERSION = YES;
336 | CLANG_WARN_COMMA = YES;
337 | CLANG_WARN_CONSTANT_CONVERSION = YES;
338 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
339 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
340 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
341 | CLANG_WARN_EMPTY_BODY = YES;
342 | CLANG_WARN_ENUM_CONVERSION = YES;
343 | CLANG_WARN_INFINITE_RECURSION = YES;
344 | CLANG_WARN_INT_CONVERSION = YES;
345 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
346 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
347 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
348 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
349 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
350 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
351 | CLANG_WARN_STRICT_PROTOTYPES = YES;
352 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
353 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
354 | CLANG_WARN_UNREACHABLE_CODE = YES;
355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
356 | COPY_PHASE_STRIP = NO;
357 | CURRENT_PROJECT_VERSION = 1.0;
358 | DEAD_CODE_STRIPPING = YES;
359 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
360 | ENABLE_NS_ASSERTIONS = NO;
361 | ENABLE_STRICT_OBJC_MSGSEND = YES;
362 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
363 | GCC_C_LANGUAGE_STANDARD = gnu11;
364 | GCC_NO_COMMON_BLOCKS = YES;
365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
367 | GCC_WARN_UNDECLARED_SELECTOR = YES;
368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
369 | GCC_WARN_UNUSED_FUNCTION = YES;
370 | GCC_WARN_UNUSED_VARIABLE = YES;
371 | GENERATE_INFOPLIST_FILE = YES;
372 | INFOPLIST_KEY_CFBundleDisplayName = Cookbook;
373 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
374 | INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.example.Cookbook;
375 | INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES;
376 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
377 | MACOSX_DEPLOYMENT_TARGET = 13.0;
378 | MARKETING_VERSION = 1.0;
379 | MTL_ENABLE_DEBUG_INFO = NO;
380 | MTL_FAST_MATH = YES;
381 | PRODUCT_NAME = "$(TARGET_NAME)";
382 | SWIFT_COMPILATION_MODE = wholemodule;
383 | SWIFT_OPTIMIZATION_LEVEL = "-O";
384 | SWIFT_VERSION = 5.0;
385 | TVOS_DEPLOYMENT_TARGET = 16.0;
386 | WATCHOS_DEPLOYMENT_TARGET = 9.0;
387 | };
388 | name = Release;
389 | };
390 | AF57980053A6180B8A055AB1 /* Debug */ = {
391 | isa = XCBuildConfiguration;
392 | buildSettings = {
393 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
394 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Cookbook.watchkit;
395 | SDKROOT = watchos;
396 | SKIP_INSTALL = YES;
397 | TARGETED_DEVICE_FAMILY = 4;
398 | };
399 | name = Debug;
400 | };
401 | CCB020EA636354610383E4B3 /* Debug */ = {
402 | isa = XCBuildConfiguration;
403 | buildSettings = {
404 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
405 | CODE_SIGN_IDENTITY = "iPhone Developer";
406 | LD_RUNPATH_SEARCH_PATHS = (
407 | "$(inherited)",
408 | "@executable_path/Frameworks",
409 | );
410 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Cookbook;
411 | SDKROOT = auto;
412 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator macosx";
413 | SUPPORTS_MACCATALYST = NO;
414 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
415 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
416 | TARGETED_DEVICE_FAMILY = "1,2,3";
417 | };
418 | name = Debug;
419 | };
420 | F578672357F094FBBE70D1F6 /* Release */ = {
421 | isa = XCBuildConfiguration;
422 | buildSettings = {
423 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
424 | CODE_SIGN_IDENTITY = "iPhone Developer";
425 | LD_RUNPATH_SEARCH_PATHS = (
426 | "$(inherited)",
427 | "@executable_path/Frameworks",
428 | );
429 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Cookbook;
430 | SDKROOT = auto;
431 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator macosx";
432 | SUPPORTS_MACCATALYST = NO;
433 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
434 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
435 | TARGETED_DEVICE_FAMILY = "1,2,3";
436 | };
437 | name = Release;
438 | };
439 | /* End XCBuildConfiguration section */
440 |
441 | /* Begin XCConfigurationList section */
442 | 0E51192C320522B946C83A31 /* Build configuration list for PBXNativeTarget "Cookbook" */ = {
443 | isa = XCConfigurationList;
444 | buildConfigurations = (
445 | CCB020EA636354610383E4B3 /* Debug */,
446 | F578672357F094FBBE70D1F6 /* Release */,
447 | );
448 | defaultConfigurationIsVisible = 0;
449 | defaultConfigurationName = Debug;
450 | };
451 | 1301D702767EC0CFA9F824AE /* Build configuration list for PBXNativeTarget "Cookbook Watch App" */ = {
452 | isa = XCConfigurationList;
453 | buildConfigurations = (
454 | AF57980053A6180B8A055AB1 /* Debug */,
455 | 0E5C3BAAE29CE190C2144FD1 /* Release */,
456 | );
457 | defaultConfigurationIsVisible = 0;
458 | defaultConfigurationName = Debug;
459 | };
460 | B6AF13F86B2F6897FD1FA06C /* Build configuration list for PBXProject "Cookbook" */ = {
461 | isa = XCConfigurationList;
462 | buildConfigurations = (
463 | 4AE5AAFEAACDDC047FEA1360 /* Debug */,
464 | 76E09144664DF93C52F8B394 /* Release */,
465 | );
466 | defaultConfigurationIsVisible = 0;
467 | defaultConfigurationName = Debug;
468 | };
469 | /* End XCConfigurationList section */
470 |
471 | /* Begin XCRemoteSwiftPackageReference section */
472 | 611005AF0B5265A88B338909 /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */ = {
473 | isa = XCRemoteSwiftPackageReference;
474 | repositoryURL = "https://github.com/liveview-native/liveview-client-swiftui";
475 | requirement = {
476 | kind = upToNextMajorVersion;
477 | minimumVersion = 0.3.0;
478 | };
479 | };
480 | 8324F855FA52F95C40E26516 /* XCRemoteSwiftPackageReference "liveview-native-live-form" */ = {
481 | isa = XCRemoteSwiftPackageReference;
482 | repositoryURL = "https://github.com/liveview-native/liveview-native-live-form";
483 | requirement = {
484 | kind = upToNextMajorVersion;
485 | minimumVersion = 0.3.0;
486 | };
487 | };
488 | /* End XCRemoteSwiftPackageReference section */
489 |
490 | /* Begin XCSwiftPackageProductDependency section */
491 | AB5B13C357FA007CBAE59E65 /* LiveViewNative */ = {
492 | isa = XCSwiftPackageProductDependency;
493 | package = 611005AF0B5265A88B338909 /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */;
494 | productName = LiveViewNative;
495 | };
496 | C565AB1EDA23B71D79160D52 /* LiveViewNativeLiveForm */ = {
497 | isa = XCSwiftPackageProductDependency;
498 | package = 8324F855FA52F95C40E26516 /* XCRemoteSwiftPackageReference "liveview-native-live-form" */;
499 | productName = LiveViewNativeLiveForm;
500 | };
501 | E561105E77C821C6381C5DBB /* LiveViewNative */ = {
502 | isa = XCSwiftPackageProductDependency;
503 | package = 611005AF0B5265A88B338909 /* XCRemoteSwiftPackageReference "liveview-client-swiftui" */;
504 | productName = LiveViewNative;
505 | };
506 | F5E1355548AE8C606CACAB0F /* LiveViewNativeLiveForm */ = {
507 | isa = XCSwiftPackageProductDependency;
508 | package = 8324F855FA52F95C40E26516 /* XCRemoteSwiftPackageReference "liveview-native-live-form" */;
509 | productName = LiveViewNativeLiveForm;
510 | };
511 | /* End XCSwiftPackageProductDependency section */
512 | };
513 | rootObject = 79F256F62D4F49097CE189BE /* Project object */;
514 | }
515 |
--------------------------------------------------------------------------------