| 155 |
183
|
|
"""
|
| 156 |
184
|
|
end
|
| 157 |
185
|
|
|
|
186
|
+ |
@doc """
|
|
187
|
+ |
A pill announcing one thing, above a hero's headline.
|
|
188
|
+ |
|
|
189
|
+ |
Three registers on one line -- a tag, the claim, a detail -- because an
|
|
190
|
+ |
announcement set at a single weight is indistinguishable from a caption. The
|
|
191
|
+ |
tag carries the only accent colour in the band, which is what makes the pill
|
|
192
|
+ |
findable before it is read.
|
|
193
|
+ |
|
|
194
|
+ |
It is a link. A pill that announces something and then does nothing when
|
|
195
|
+ |
pressed spends a reader's attention and returns none of it, so `navigate`,
|
|
196
|
+ |
`patch`, or `href` is required.
|
|
197
|
+ |
"""
|
|
198
|
+ |
attr :tag, :any, default: "New", doc: "the leading badge; nil to omit"
|
|
199
|
+ |
attr :lead, :string, required: true, doc: "what is being announced"
|
|
200
|
+ |
attr :detail, :string, default: nil, doc: "the quieter half of the line"
|
|
201
|
+ |
attr :glyph, :any, default: "play-sm", doc: "the trailing glyph; nil to omit"
|
|
202
|
+ |
attr :class, :any, default: nil
|
|
203
|
+ |
attr :rest, :global, include: ~w(navigate patch href target rel)
|
|
204
|
+ |
|
|
205
|
+ |
def announce(assigns) do
|
|
206
|
+ |
~H"""
|
|
207
|
+ |
<.link class={["announce", @class]} {@rest}>
|
|
208
|
+ |
<span :if={@tag} class="announce__tag">{@tag}</span>
|
|
209
|
+ |
<span class="announce__text">
|
|
210
|
+ |
<span class="announce__lead">{@lead}</span><span
|
|
211
|
+ |
:if={@detail}
|
|
212
|
+ |
class="announce__separator"
|
|
213
|
+ |
aria-hidden="true"
|
|
214
|
+ |
> • </span><span :if={@detail} class="announce__detail">{@detail}</span>
|
|
215
|
+ |
</span>
|
|
216
|
+ |
<span :if={@glyph} class="announce__glyph" aria-hidden="true">
|
|
217
|
+ |
<UI.icon name={@glyph} />
|
|
218
|
+ |
</span>
|
|
219
|
+ |
</.link>
|
|
220
|
+ |
"""
|
|
221
|
+ |
end
|
|
222
|
+ |
|
|
223
|
+ |
@doc """
|
|
224
|
+ |
One shell command, framed, and copied to the clipboard when pressed.
|
|
225
|
+ |
|
|
226
|
+ |
The whole frame is the control rather than a button beside it: the command is
|
|
227
|
+ |
not there to be read character by character, it is there to be taken, and a
|
|
228
|
+ |
small target next to a long piece of text asks the reader to aim at the
|
|
229
|
+ |
smaller of the two.
|
|
230
|
+ |
|
|
231
|
+ |
A command too long for the viewport scrolls inside the frame and fades out
|
|
232
|
+ |
under the glyph rather than wrapping, so the band keeps one line of monospace
|
|
233
|
+ |
at every width. The confirmation lives on the element as `data-copied`,
|
|
234
|
+ |
because it is presentational and per-visitor and the server has no stake in
|
|
235
|
+ |
it.
|
|
236
|
+ |
"""
|
|
237
|
+ |
attr :id, :string, required: true
|
|
238
|
+ |
attr :command, :string, required: true
|
|
239
|
+ |
attr :prompt, :string, default: "$", doc: "the shell prompt drawn before the command"
|
|
240
|
+ |
attr :label, :string, default: "Copy the install command"
|
|
241
|
+ |
attr :class, :any, default: nil
|
|
242
|
+ |
|
|
243
|
+ |
def install_command(assigns) do
|
|
244
|
+ |
~H"""
|
|
245
|
+ |
<div class={["install-command", @class]}>
|
|
246
|
+ |
<button
|
|
247
|
+ |
id={@id}
|
|
248
|
+ |
type="button"
|
|
249
|
+ |
class="install-command__button"
|
|
250
|
+ |
data-copied="false"
|
|
251
|
+ |
data-copy-text={@command}
|
|
252
|
+ |
aria-label={@label}
|
|
253
|
+ |
phx-hook=".CopyCommand"
|
|
254
|
+ |
>
|
|
255
|
+ |
<code class="install-command__text">
|
|
256
|
+ |
<span class="install-command__prompt" aria-hidden="true">{@prompt}</span>{@command}
|
|
257
|
+ |
</code>
|
|
258
|
+ |
<span class="install-command__glyph" aria-hidden="true">
|
|
259
|
+ |
<UI.icon name="copy" class="install-command__idle" />
|
|
260
|
+ |
<UI.icon name="check" class="install-command__done" />
|
|
261
|
+ |
</span>
|
|
262
|
+ |
</button>
|
|
263
|
+ |
</div>
|
|
264
|
+ |
<script :type={Phoenix.LiveView.ColocatedHook} name=".CopyCommand">
|
|
265
|
+ |
export default {
|
|
266
|
+ |
mounted() {
|
|
267
|
+ |
this.el.addEventListener("click", async () => {
|
|
268
|
+ |
try {
|
|
269
|
+ |
await navigator.clipboard.writeText(this.el.dataset.copyText)
|
|
270
|
+ |
} catch (_error) {
|
|
271
|
+ |
return
|
|
272
|
+ |
}
|
|
273
|
+ |
this.el.dataset.copied = "true"
|
|
274
|
+ |
clearTimeout(this.resetTimer)
|
|
275
|
+ |
this.resetTimer = setTimeout(() => {
|
|
276
|
+ |
this.el.dataset.copied = "false"
|
|
277
|
+ |
}, 1600)
|
|
278
|
+ |
})
|
|
279
|
+ |
},
|
|
280
|
+ |
destroyed() {
|
|
281
|
+ |
clearTimeout(this.resetTimer)
|
|
282
|
+ |
}
|
|
283
|
+ |
}
|
|
284
|
+ |
</script>
|
|
285
|
+ |
"""
|
|
286
|
+ |
end
|
|
287
|
+ |
|
| 158 |
288
|
|
@doc """
|
| 159 |
289
|
|
A grid of short capability statements.
|
| 160 |
290
|
|
|