Skip to content

Commit 84f3aea

Browse files
committed
Improve color picker
1 parent 4bd2382 commit 84f3aea

File tree

5 files changed

+179
-33
lines changed

5 files changed

+179
-33
lines changed

apps/components_guide_web/lib/components_guide_web/live/color.ex

Lines changed: 115 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ defmodule ComponentsGuideWeb.ColorLive do
22
use ComponentsGuideWeb, :live_view
33
alias ComponentsGuideWeb.StylingHelpers
44

5+
@update_url_delay 500
6+
57
defmodule State do
68
defstruct color: {:lab, 50, 100, -128}
79

@@ -25,17 +27,19 @@ defmodule ComponentsGuideWeb.ColorLive do
2527
%State{color: color}
2628
end
2729

30+
@lab_separator <<"~"::utf8>>
31+
2832
def decode(:lab, input) when is_binary(input) do
29-
{l, <<","::utf8>> <> rest} = Integer.parse(input)
30-
{a, <<","::utf8>> <> rest} = Integer.parse(rest)
33+
{l, @lab_separator <> rest} = Integer.parse(input)
34+
{a, @lab_separator <> rest} = Integer.parse(rest)
3135
{b, ""} = Integer.parse(rest)
3236

3337
%__MODULE__{color: {:lab, l, a, b}}
3438
end
3539

36-
def encode(%__MODULE__{color: color}) do
37-
{:lab,l,a,b} = color
38-
"#{l},#{a},#{b}"
40+
def encode(%__MODULE__{color: color}) do
41+
{:lab, l, a, b} = color
42+
"#{l}#{@lab_separator}#{a}#{@lab_separator}#{b}"
3943
end
4044

4145
def set_color(state = %__MODULE__{}, color), do: %{state | color: color}
@@ -44,7 +48,8 @@ defmodule ComponentsGuideWeb.ColorLive do
4448
def a(%__MODULE__{color: {:lab, _, a, _}}), do: a
4549
def b(%__MODULE__{color: {:lab, _, _, b}}), do: b
4650

47-
def css(%__MODULE__{color: color}), do: StylingHelpers.to_css(color)
51+
def css_srgb(%__MODULE__{color: color}), do: StylingHelpers.to_css(color, :srgb)
52+
def css(%__MODULE__{color: color}), do: StylingHelpers.to_css(color, nil)
4853

4954
defp to_srgb(%__MODULE__{color: color}) do
5055
{:srgb, r, g, b} = color |> StylingHelpers.convert(:srgb) |> StylingHelpers.clamp()
@@ -65,23 +70,92 @@ defmodule ComponentsGuideWeb.ColorLive do
6570
end
6671
end
6772

73+
defp interpolate(t, {lowest, highest}) do
74+
(highest - lowest) * t + lowest
75+
end
76+
6877
def render(assigns) do
6978
swatch_size = 100
7079
{:lab, l, a, b} = assigns.state.color
7180

72-
gradient =
73-
Styling.linear_gradient("150grad", [
74-
{:lab, l * 1.5, a * 1.2, b * 0.8},
75-
{:lab, l, a, b},
76-
{:lab, l * 0.5, a * 0.8, b * 1.2}
77-
])
81+
l_steps = 10
82+
83+
l_gradient =
84+
Styling.linear_gradient(
85+
"150grad",
86+
for(n <- 0..l_steps, do: {:lab, n * (100 / l_steps), a, b})
87+
)
88+
89+
l_gradient_svg =
90+
Styling.svg_linear_gradient(
91+
"rotate(45)",
92+
for(n <- 0..l_steps, do: {:lab, interpolate(n / l_steps, {0.0, 100.0}), a, b})
93+
)
94+
95+
a_steps = 10
96+
97+
a_gradient =
98+
Styling.linear_gradient(
99+
"150grad",
100+
for(n <- 0..a_steps, do: {:lab, l, interpolate(n / a_steps, {-127.0, 127.0}), b})
101+
)
102+
103+
a_gradient_svg =
104+
Styling.svg_linear_gradient(
105+
"rotate(45)",
106+
for(n <- 0..a_steps, do: {:lab, l, interpolate(n / a_steps, {-127.0, 127.0}), b})
107+
)
108+
109+
b_steps = 10
110+
111+
b_gradient =
112+
Styling.linear_gradient(
113+
"150grad",
114+
for(n <- 0..b_steps, do: {:lab, l, a, interpolate(n / b_steps, {-127.0, 127.0})})
115+
)
116+
117+
b_gradient_svg =
118+
Styling.svg_linear_gradient(
119+
"rotate(45)",
120+
for(n <- 0..b_steps, do: {:lab, l, a, interpolate(n / b_steps, {-127.0, 127.0})})
121+
)
78122

79123
~L"""
80124
<article class="text-2xl max-w-lg mx-auto text-white">
81125
<svg width="<%= swatch_size %>" height="<%= swatch_size %>" viewbox="0 0 1 1">
82-
<rect fill="<%= State.hex(@state) %>" width="1" height="1" />
126+
<rect fill="<%= State.css_srgb(@state) %>" width="1" height="1" />
83127
</svg>
84-
<div style="width: 100px; height: 100px; background-image: <%= gradient %>"></div>
128+
<div class="flex">
129+
<svg viewBox="0 0 1 1" width="100" height="100">
130+
<defs>
131+
<%= l_gradient_svg %>
132+
</defs>
133+
<rect width="1" height="1" fill="url('#myGradient')" />
134+
<circle cx="<%= l / 100.0 %>" cy="<%= l / 100.0 %>" r="0.05" fill="white" stroke="black" stroke-width="0.01" />
135+
</svg>
136+
<svg viewBox="0 0 1 1" width="100" height="100">
137+
<defs>
138+
<%= a_gradient_svg %>
139+
</defs>
140+
<rect width="1" height="1" fill="url('#myGradient')" />
141+
<circle cx="<%= (a / 127.0) / 2.0 + 0.5 %>" cy="<%= (a / 127.0) / 2.0 + 0.5 %>" r="0.05" fill="white" stroke="black" stroke-width="0.01" />
142+
</svg>
143+
<svg viewBox="0 0 1 1" width="100" height="100">
144+
<defs>
145+
<%= b_gradient_svg %>
146+
</defs>
147+
<rect width="1" height="1" fill="url('#myGradient')" />
148+
<circle cx="<%= (b / 127.0) / 2.0 + 0.5 %>" cy="<%= (b / 127.0) / 2.0 + 0.5 %>" r="0.05" fill="white" stroke="black" stroke-width="0.01" />
149+
</svg>
150+
<!--<div style="width: 100px; height: 100px; background-image: <%= l_gradient %>"></div>-->
151+
<!--<div style="width: 100px; height: 100px; background-image: <%= a_gradient %>"></div>-->
152+
<div style="width: 100px; height: 100px; background-image: <%= b_gradient %>"></div>
153+
<svg width="100" height="100">
154+
<foreignObject>
155+
<div style="width: 100px; height: 100px; background-image: <%= b_gradient %>"></div>
156+
</foreignObject>
157+
</svg>
158+
</div>
85159
<form phx-change="lab_changed" class="flex flex-col">
86160
<label>
87161
L
@@ -103,16 +177,16 @@ defmodule ComponentsGuideWeb.ColorLive do
103177
<dt class="font-bold">Hex:
104178
<dd><%= State.hex(@state) %>
105179
<dt class="font-bold">CSS:
106-
<dd><pre class="text-base whitespace-pre-wrap"><code><%= State.css(@state) %></code></pre>
180+
<dd><pre class="text-base whitespace-pre-wrap"><code><%= State.css_srgb(@state) %></code></pre>
107181
<dt class="font-bold">Gradient CSS:
108-
<dd><pre class="text-base whitespace-pre-wrap"><code><%= gradient %></code></pre>
182+
<dd><pre class="text-base whitespace-pre-wrap"><code><%= l_gradient %></code></pre>
109183
</dl>
110184
</article>
111185
"""
112186
end
113187

114188
def mount(_params, _session, socket) do
115-
{:ok, assign(socket, state: %State{})}
189+
{:ok, assign(socket, state: %State{}, tref: nil)}
116190
end
117191

118192
def handle_params(%{"definition" => definition}, _path, socket) do
@@ -125,18 +199,37 @@ defmodule ComponentsGuideWeb.ColorLive do
125199
{:noreply, socket |> assign(:state, state)}
126200
end
127201

202+
def handle_info(:update_url, socket) do
203+
state = socket.assigns.state
204+
encoded = State.encode(state)
205+
206+
{:noreply,
207+
socket
208+
|> assign(:tref, nil)
209+
|> push_patch(to: Routes.color_path(socket, :lab, encoded), replace: true)}
210+
end
211+
128212
def handle_event("lab_changed", %{"l" => l, "a" => a, "b" => b}, socket) do
129213
l = l |> String.to_integer()
130214
a = a |> String.to_integer()
131215
b = b |> String.to_integer()
132216

133217
state = socket.assigns.state |> State.set_color({:lab, l, a, b})
134-
encoded = State.encode(state)
135218

136-
# TODO: throttle for Safari’s
219+
case socket.assigns.tref do
220+
nil -> nil
221+
tref -> :timer.cancel(tref)
222+
end
223+
224+
# Throttle for Safari’s
137225
# SecurityError: Attempt to use history.replaceState() more than 100 times per 30 seconds
138-
{:noreply,
139-
socket
140-
|> push_patch(to: Routes.color_path(socket, :lab, encoded), replace: true)}
226+
{:ok, tref} = :timer.send_after(@update_url_delay, :update_url)
227+
228+
{
229+
:noreply,
230+
socket
231+
|> assign(:state, state)
232+
|> assign(:tref, tref)
233+
}
141234
end
142235
end

apps/components_guide_web/lib/components_guide_web/templates/accessibility_first/properties-cheatsheet.html.eex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
theme = %Theme{text_color: "blue"}
33
[h2, h3, h4] = Theme.headings(theme)
44
%>
5-
<header class="text-white" style="background: <%= header_background %>;">
5+
<header class="text-white" style="background: <%= header_background() %>;">
66
<div class="container px-6 pt-6 pb-6">
77
<h1 class="mx-auto max-w-4xl text-4xl text-center font-bold leading-tight text-shadow">
88
<%= "Accessible Properties Cheatsheet" %>
@@ -36,11 +36,11 @@
3636

3737
<p>You can hide using a large range of techniques. What you first want to ask is, from whom do I want to hide the content?
3838

39-
<ul>
40-
<li><%= line "Hide to **everyone**." %>
41-
<li><%= line "**Screen reader affordance:** hide to sighted users, but show to screen reader users." %>
42-
<li><%= line "**Visual affordance:** hide to screen reader users, but show to sighted users." %>
43-
</ul>
39+
<%= list [
40+
"Hide to **everyone**.",
41+
"**Screen reader affordance:** hide to sighted users, but show to screen reader users.",
42+
"**Visual affordance:** hide to screen reader users, but show to sighted users."
43+
] %>
4444

4545
<section aria-labelledby="hidden-all-heading">
4646
<%= h3.("Hide to everyone", id: "hidden-all-heading") %>

apps/components_guide_web/lib/components_guide_web/templates/accessibility_first/widgets-cheatsheet.html.eex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
<li><%= link "Radio", to: "#radio" %>
2626
<li><%= link "Range", to: "#range" %>
2727
<li><%= link "Tabs", to: "#tabs" %>
28+
<li><%= link "Modal Dialog", to: "#dialog" %>
29+
<li role=separator>
2830
<li><%= link("List of roles 🔗", to: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques") %>
2931
<li><%= link("Queries 🔗", to: "https://testing-library.com/docs/dom-testing-library/api-queries") %>
3032
<li><%= link("Matchers 🔗", to: "https://github.com/testing-library/jest-dom") %>

apps/components_guide_web/lib/components_guide_web/views/accessibility_first_view.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ defmodule ComponentsGuideWeb.AccessibilityFirstView do
3333
"""
3434
end
3535

36+
def list(items) do
37+
~E"""
38+
<ul>
39+
<%= for item <- items do %>
40+
<li><%= line(item) %>
41+
<% end %>
42+
</ul>
43+
"""
44+
end
45+
3646
defmodule Theme do
3747
defstruct text_color: "blue"
3848

apps/components_guide_web/lib/components_guide_web/views/styling_helpers.ex

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ defmodule ComponentsGuideWeb.StylingHelpers do
33
Conveniences for creating gradient backgrounds
44
"""
55

6+
use Phoenix.HTML
7+
68
# Source: https://github.com/Evercoder/culori
79

810
defp convert_component({:linear_srgb, c}, :srgb) do
@@ -132,15 +134,21 @@ defmodule ComponentsGuideWeb.StylingHelpers do
132134
{:srgb, r |> clamp_0_1(), g |> clamp_0_1(), b |> clamp_0_1()}
133135
end
134136

135-
def to_css(color = {:srgb, _, _, _}) do
137+
def to_css(color = {:srgb, _, _, _}, :srgb) do
136138
{:srgb, r, g, b} = clamp(color)
137139
"rgba(#{(r * 255) |> round},#{(g * 255) |> round},#{(b * 255) |> round},1)"
138140
end
139141

140-
def to_css(color_tuple) when is_tuple(color_tuple) and elem(color_tuple, 0) in [:lab] do
141-
color_tuple |> convert(:srgb) |> to_css()
142+
def to_css(color, :srgb) do
143+
color |> convert(:srgb) |> to_css(:srgb)
144+
end
145+
146+
def to_css({:lab, l, a, b}, nil) do
147+
"lab(#{l |> round}% #{a |> round} #{b |> round})"
142148
end
143149

150+
def to_css(color), do: to_css(color, :srgb)
151+
144152
def linear_gradient(angle, colors) when is_list(colors) do
145153
linear_gradient(angle, :array.from_list(colors))
146154
end
@@ -154,13 +162,46 @@ defmodule ComponentsGuideWeb.StylingHelpers do
154162
:array.foldr(
155163
fn index, color, list ->
156164
percentage = "#{index / max * 100}%"
157-
["#{color |> to_css()} #{percentage}" | list]
165+
["#{color |> to_css(:srgb)} #{percentage}" | list]
158166
end,
159167
[],
160168
colors_array
161169
)
162-
|> Enum.join(",")
170+
|> Enum.join(", ")
163171

164172
"linear-gradient(#{angle}, #{colors_css})"
165173
end
174+
175+
def svg_linear_gradient(angle, colors) when is_list(colors) do
176+
svg_linear_gradient(angle, :array.from_list(colors))
177+
end
178+
179+
def svg_linear_gradient(angle, colors_array) do
180+
true = :array.is_array(colors_array)
181+
182+
max = :array.size(colors_array) - 1
183+
184+
stops =
185+
:array.foldr(
186+
fn index, color, list ->
187+
percentage = "#{index / max * 100}%"
188+
189+
xml = ~E"""
190+
<stop offset="<%= percentage %>" stop-color="<%= to_css(color, :srgb) %>" />
191+
"""
192+
193+
[
194+
xml | list
195+
]
196+
end,
197+
[],
198+
colors_array
199+
)
200+
201+
~E"""
202+
<linearGradient id="myGradient" gradientTransform="scale(1.414) <%= angle %>">
203+
<%= stops %>
204+
</linearGradient>
205+
"""
206+
end
166207
end

0 commit comments

Comments
 (0)