<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: John P. Rouillard</title>
    <description>The latest articles on Forem by John P. Rouillard (@rouilj).</description>
    <link>https://forem.com/rouilj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F451676%2Fe8e1cabc-ddbe-446e-90ed-87a386cf27c2.png</url>
      <title>Forem: John P. Rouillard</title>
      <link>https://forem.com/rouilj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rouilj"/>
    <language>en</language>
    <item>
      <title>How does your customer help you?</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Thu, 14 Nov 2024 01:09:32 +0000</pubDate>
      <link>https://forem.com/rouilj/how-does-your-customer-help-you-18oj</link>
      <guid>https://forem.com/rouilj/how-does-your-customer-help-you-18oj</guid>
      <description>&lt;p&gt;Many companies are cutting back and delaying new projects after the recent election. As a new solution provider, how do you create a win-win situation for customers who might worry about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;quality of the solution&lt;/li&gt;
&lt;li&gt;satisfaction with the result&lt;/li&gt;
&lt;li&gt;cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How can you add value for the customer by reducing risk or cost?&lt;/p&gt;

&lt;p&gt;I propose 4 types of customers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Regular customer: straight work for hire.&lt;/li&gt;
&lt;li&gt;Reference customer: provides a private reference for my work to a potential customer.&lt;/li&gt;
&lt;li&gt;Sponsor customer: the company is publically promoted as a user of the product.&lt;/li&gt;
&lt;li&gt;Partner customer: actively promotes the product within their market.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Regular customer
&lt;/h2&gt;

&lt;p&gt;The customer receives a proposal for their project. Engages in discussions regarding the scope of work, pricing, and other details. Once the contract concludes, there is no continued relationship with the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Customer
&lt;/h2&gt;

&lt;p&gt;A reference customer starts as a regular customer. As part of the contract, they agree to talk with potential clients to discuss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the project scope,&lt;/li&gt;
&lt;li&gt;the benefits of the solution,&lt;/li&gt;
&lt;li&gt;and the overall business impact of the work performed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contracts are protected by a non-disclosure agreement to safeguard the confidentiality of references. This allows prospective clients to better understand the value proposition. But it prohibits them from sharing any proposal-related information outside their organization.&lt;/p&gt;

&lt;p&gt;In exchange for serving as a reference, these customers receive discounted rates on their contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sponsor Customer
&lt;/h2&gt;

&lt;p&gt;A sponsor customer is a reference customer who has consented to publicly associate their company name with the solution/product. This association includes prominent visibility on websites, emails, and various marketing materials.&lt;/p&gt;

&lt;p&gt;Sponsor customers receive a greater discount on their contracts compared to reference customers. They may enjoy other benefits, such as enhanced support agreements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partner Customer
&lt;/h2&gt;

&lt;p&gt;A partner customer can be in any of the other customer categories. Unlike the other customers, who take a passive approach to supporting the product, these customers actively advocate for the product.&lt;/p&gt;

&lt;p&gt;They share their positive experiences with others in the same industry. This strategy is particularly effective when the product offered is not a primary focus or distinguishing factor for the customer's business.&lt;/p&gt;

&lt;p&gt;Rather than receiving discounts on contracts, these customers benefit from additional revenue when a client they refer decides to purchase the solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Offering customers an ongoing role in the success of your business can reduce their concerns about your solution. This win-win approach to negotiating contracts can also reach new clients and help answer the concerns of new clients.&lt;/p&gt;

&lt;p&gt;What are your thoughts about these customer categories?&lt;br&gt;
What tools/techniques do you use to help customers get&lt;br&gt;
to yes?&lt;/p&gt;

&lt;p&gt;(A good reference on negotiating is &lt;a href="https://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757" rel="noopener noreferrer"&gt;https://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757&lt;/a&gt;.)&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Copy issue reference to clipboard enhancement</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Thu, 09 Nov 2023 00:19:55 +0000</pubDate>
      <link>https://forem.com/rouilj/copy-issue-reference-to-clipboard-enhancement-4ka1</link>
      <guid>https://forem.com/rouilj/copy-issue-reference-to-clipboard-enhancement-4ka1</guid>
      <description>&lt;p&gt;On the &lt;a href="https://sourceforge.net/p/roundup/mailman/search/?q=Feature+Request+%3A+Copy+Issue+id+and+title+separated+by+%27%3B+%27+to+the+clipboard&amp;amp;mail_list=roundup-users" rel="noopener noreferrer"&gt;Roundup Issue Tracker mailing list&lt;/a&gt; a user asked for a mechanism to create a reference for the current issue and copy it to the clipboard.&lt;/p&gt;

&lt;p&gt;The request was for the reference to look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  issue2345: title of issue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In Roundup, object designators like &lt;code&gt;issue2345&lt;/code&gt; are automatically hyperlinked to the corresponding object.&lt;/p&gt;

&lt;p&gt;The code I recommended used a button to trigger this operation. It also used the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API" rel="noopener noreferrer"&gt;clipboard API&lt;/a&gt;. When clicking on the button (or hitting &lt;code&gt;space&lt;/code&gt; or &lt;code&gt;return&lt;/code&gt; while focused on the button) the reference format above will be copied to the clipboard. Triggering the clipboard API must be done by a user interaction. For example: activating a button. If the browser does not support the clipboard API, a simple &lt;code&gt;alert()&lt;/code&gt; is shown.&lt;/p&gt;

&lt;p&gt;In addition to copying to the clipboard, the user gets feedback when the text of the button changes to "Reference copied". Then it resets to the original message after 2 seconds. Since clicking the button multiple times is idempotent, there is no sense in disabling or debouncing the button.&lt;/p&gt;

&lt;p&gt;Note, this is unlikely to be a11y compliant as I don't think the change of button text is announced. If anybody has some ideas on how to make this more compliant, leave them in the comments. Maybe using &lt;code&gt;aria-live="polite"&lt;/code&gt; or &lt;code&gt;"assertive"&lt;/code&gt; on the &lt;code&gt;button&lt;/code&gt; element would do the trick?&lt;/p&gt;

&lt;p&gt;Here is the code I suggested:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"copyreference"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="na"&gt;tal:condition=&lt;/span&gt;&lt;span class="s"&gt;"context/is_edit_ok"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   Copy Reference
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;tal:attributes=&lt;/span&gt;&lt;span class="s"&gt;"nonce request/client/client_nonce"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;crb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#copyreference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;crb&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nx"&gt;crb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Clipboard is not available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;issueDesignator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;issueTitleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issueDesignator&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issueTitleText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;originalCrbInnerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;
        &lt;span class="nx"&gt;crb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reference copied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;crb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalCrbInnerText&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;a href="https://www.roundup-tracker.org/docs/features.html" rel="noopener noreferrer"&gt;flexibility of Roundup&lt;/a&gt; allows the administrator to rewrite all of the HTML used in the web interface. As a result, I based my guess of CSS selectors on the &lt;a href="https://sourceforge.net/p/roundup/code/ci/default/tree/share/roundup/templates/classic/html/issue.item.html" rel="noopener noreferrer"&gt;HTML generated by the classic &lt;code&gt;issue.item.html&lt;/code&gt; TAL template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The button could be placed anywhere on the page and the script (including the nonce required by the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" rel="noopener noreferrer"&gt;CSP&lt;/a&gt;) would be placed anywhere after the button. Probably at the end of the page so it doesn't block rendering.&lt;/p&gt;

&lt;p&gt;The classic structure of the issue display page for users with editing capability included an input with the id &lt;code&gt;title&lt;/code&gt;. This is retrieved using the id and included in the string written to the clipboard.&lt;/p&gt;

&lt;p&gt;A user without editing capability for the issue (or without editing capability for the title attribute of an issue) does not have an input with &lt;code&gt;id="title"&lt;/code&gt;.&lt;br&gt;
In these cases, the admin would have to modify the &lt;code&gt;issue.item.html&lt;/code&gt; template to add an id of title to the enclosing element. Then the code above would be modified to replace:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;issueTitleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;issueTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input#title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;issueTitle&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;issueTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="nx"&gt;issueTitleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issueTitle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;issueTitleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issueTitle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In my original example, the button element is generated only if the user can edit the issue. Removing the &lt;code&gt;tal:condition&lt;/code&gt; attribute would always display the button.&lt;/p&gt;

&lt;p&gt;There is nothing with an id or CSS selector that contains the object designator. I use the final element of the path of the URL to get the designator.&lt;/p&gt;

&lt;p&gt;I chose to use the &lt;a href="https://en.wikipedia.org/wiki/Immediately-invoked_function_expression" rel="noopener noreferrer"&gt;IIFE&lt;/a&gt; code structure. This allows me to &lt;a href="https://gomakethings.com/the-early-return-pattern-in-javascript/" rel="noopener noreferrer"&gt;use the early return pattern&lt;/a&gt; if the button is not found. If anybody knows how to do the equivalent of an early return without an IIFE or other function in a script tag, leave your trick in the comments.&lt;/p&gt;

&lt;p&gt;If you use the Roundup Issue Tracker and have your own enhancements, feel free to mention them in the comments.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
      &lt;div class="c-embed__cover"&gt;
        &lt;a href="https://www.roundup-tracker.org/?ref=dev_to" class="c-link s:max-w-50 align-middle" rel="noopener noreferrer"&gt;
          &lt;img alt="" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.roundup-tracker.org%2F_images%2Findex_logged_out.png" height="auto" class="m-0"&gt;
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;div class="c-embed__body"&gt;
      &lt;h2 class="fs-xl lh-tight"&gt;
        &lt;a href="https://www.roundup-tracker.org/?ref=dev_to" rel="noopener noreferrer" class="c-link"&gt;
          Roundup Issue Tracker - Roundup 2.4.0 documentation
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;p class="truncate-at-3"&gt;
          A simple-to-use and -install issue-tracking system with command-line, web, REST, XML-RPC and e-mail interfaces. Adaptable to many use cases. Allows you to customize the look and feel and implement different workflows.
        &lt;/p&gt;
      &lt;div class="color-secondary fs-s flex items-center"&gt;
        roundup-tracker.org
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>To Release or Not to Release: What's a Good Schedule</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Mon, 12 Jun 2023 17:15:00 +0000</pubDate>
      <link>https://forem.com/rouilj/to-release-or-not-to-release-whats-a-good-schedule-11fk</link>
      <guid>https://forem.com/rouilj/to-release-or-not-to-release-whats-a-good-schedule-11fk</guid>
      <description>&lt;p&gt;Shakespeare once said "A rose by any other name would smell as sweet". I wonder if something similar can be said for software releases.&lt;/p&gt;

&lt;p&gt;I am planning a new release of the opensource &lt;a href="https://www.roundup-tracker.org"&gt;Roundup Issue Tracker&lt;/a&gt;. Historically, release schedules were on an as needed basis. Before 2006 (when version 1.0.0 was released) there were multiple years with more than 10 releases (including alpha/beta and other pre releases).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Releases&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Releases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2009&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2007&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2004&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2003&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But there were no releases from 2013 until 2016. Also, there was a gap in 2017. There was significant development done during those years without releases. But only those willing to run code from the repository were able to benefit.  In 2018 I took over as release manager and chose to start doing yearly releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Release
&lt;/h2&gt;

&lt;p&gt;With opensource projects, there are many reasons to release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;get features/bug fixes to users&lt;/li&gt;
&lt;li&gt;thank contributors&lt;/li&gt;
&lt;li&gt;increase project visibility&lt;/li&gt;
&lt;li&gt;establish expectations&lt;/li&gt;
&lt;li&gt;provide deadlines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As I mentioned above, producing a release makes the work of contributors (developers, documentation writers, people reporting issues) isn't generally available. Frequent releases align with my preference to reduce &lt;a href="https://en.wikipedia.org/wiki/Work_in_process"&gt;WIP (work in progress)&lt;/a&gt;. Thanking the contributors by releasing their work helps drive enthusiasm and engagement.&lt;/p&gt;

&lt;p&gt;Announcements on multiple platforms accompany each release. When I see another issue tracking/bug reporting program release announcement, I experience &lt;a href="https://www.merriam-webster.com/dictionary/FOMO"&gt;FOMO&lt;/a&gt; over the marketing provided by their announcement.&lt;/p&gt;

&lt;p&gt;A regular release cadence helps users to schedule time for testing or upgrading the software. Also, regularly scheduled releases encourage  developers to finish their work and get it into a release. This deadline also drives me to go through all the steps to generate a beta and final release. This year (2023) will be my 6th yearly release.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Release
&lt;/h2&gt;

&lt;p&gt;Although I do believe that reducing WIP is important, Roundup is not provided as a SAS (software as a service). It is deployed on-prem and meant to be customized. Some changes to Roundup require changes to a customized installation as part of the upgrade. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upgrade from Python 2 to Python 3 can require rewriting a tracker's business logic&lt;/li&gt;
&lt;li&gt;support for newer databases versions can require table alteration (sometimes manual, sometimes programmatic using the &lt;code&gt;roundup-admin migrate&lt;/code&gt; command)&lt;/li&gt;
&lt;li&gt;security improvements and CSRF protection require changes to customized HTML templates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I mentioned my FOMO when seeing other products release multiple times a year. I often see releases with 5 or so changes.  Often these changes fix cosmetic issues or broken fields in the web interface.&lt;/p&gt;

&lt;p&gt;With Roundup, many users have redesigned/replaced part of the interface. A release only supplies changes to the reference interfaces. The Roundup administrator needs to merge the reference changes into their production interface. In Roundup, changes can be implemented in production without requiring a release. This separation of concerns between the Roundup core and a customizable tracker makes Roundup more flexible. But, increases the work required during an upgrade.&lt;/p&gt;

&lt;p&gt;Because of how customizable Roundup is, I try to balance between WIP and the churn caused by frequent releases. Yearly seems to be a sweet spot. There are occasions when a new core feature needs a change/bug fix. In that case the user has a choice to wait for a new release or use the most recent commit to the development repository.&lt;/p&gt;

&lt;p&gt;If Roundup was a SAS with more limited customizability, we could do canary deploys directly from the repository. This would positively affect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;getting features/bug fixes to users&lt;/li&gt;
&lt;li&gt;thank contributors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;but would also reduce the utility of Roundup. Setting up a &lt;a href="https://gettingthingsdone.com/"&gt;Getting Things Done&lt;/a&gt; tracker for his/her family would find it very difficult in a SAS based model. Also, the tempo of releases in a SAS based model may not align with the user's needs for a stable platform.&lt;/p&gt;

&lt;p&gt;If Shakespeare were a software writer would he have penned "Would software by another release schedule smell as sweet?". Leave your thoughts in the comments.&lt;/p&gt;

&lt;p&gt;(&lt;a href="https://unsplash.com/photos/P1kP-BfIelI"&gt;Cover Image by Glen Carrie&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>discuss</category>
      <category>opensource</category>
      <category>development</category>
    </item>
    <item>
      <title>Mobile Web Page Debugging Without a Desktop or USB Cable</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Sun, 19 Mar 2023 17:01:11 +0000</pubDate>
      <link>https://forem.com/rouilj/mobile-web-page-debugging-without-a-desktop-or-usb-cable-4ckl</link>
      <guid>https://forem.com/rouilj/mobile-web-page-debugging-without-a-desktop-or-usb-cable-4ckl</guid>
      <description>&lt;p&gt;One of the things I have to do when developing a new tracker for the &lt;a href="https://www.roundup-tracker.org?ref=dev_to"&gt;Roundup Issue Tracker&lt;/a&gt; is test on mobile browsers. When I run into issues, I used to hook up a remote debugger. This usually requires finding a USB cable as wifi is less reliable.&lt;/p&gt;

&lt;p&gt;I really wish that mobile browsers had a native console that I could use to see &lt;code&gt;console.log&lt;/code&gt; output and javascript errors. While not native, &lt;a href="https://github.com/c-kick/mobileConsole"&gt;MobileConsole by hnldesign&lt;/a&gt; provides quite a bit of power by including one script and one style tag in my HTML page. At 45k uncompressed (17k gzipped) it's sized between the smallest and largest mobile consoles I found.&lt;/p&gt;

&lt;p&gt;It can be seen at the bottom of the web page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O1Zhf3E_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jmkbsekrqfzsmuy1v75p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O1Zhf3E_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jmkbsekrqfzsmuy1v75p.png" alt="Phone screenshot showing the MobileConsole displayed on the bottom half of the screen. The top half displays a web page. There is a movable divider between the two. The console is showing log messages from the page along with the line number where they were logged" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It supports the usual console output functions (&lt;code&gt;error&lt;/code&gt;, ... &lt;code&gt;debug&lt;/code&gt;). Plus it supports &lt;code&gt;console.time&lt;/code&gt;/&lt;code&gt;timeEnd&lt;/code&gt; and &lt;code&gt;console.trace&lt;/code&gt; along with the &lt;code&gt;group&lt;/code&gt; family. It also displays objects collapsed and allows you to toggle them open. The console can be resized allowing you to reduce the screen real-estate used on a mobile device.&lt;/p&gt;

&lt;p&gt;It has some areas for improvement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it only opens objects one level deep. So an array in an object is displayed but you can't expand the array further to get access to its elements.&lt;/li&gt;
&lt;li&gt;grouping is not nested or collapsible. But this is noted as an area for improvement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do a lot of logging, you might want to download the library, open your editor, and apply &lt;a href="https://github.com/c-kick/mobileConsole/issues/2"&gt;a change that reduces redraws triggered by console output&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One thing to note is that running commands in the console will not work if you are using a Content Security Policy (CSP). Any CSP that does not allow &lt;code&gt;'unsafe-eval'&lt;/code&gt; will prevent console commands from running. But at least you will see the error in the console 8-).&lt;/p&gt;

&lt;p&gt;There are other mobile consoles as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ljcucc/mobileConsole"&gt;mobileConsole&lt;/a&gt; is the smallest at 5k, but appears to only support console.log. Documentation is sparse and there is no demo.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/B1naryStudio/js-mobile-console"&gt;B1naryStudio js-mobile-console&lt;/a&gt; - is 6k in size. It has methods to show/hide, enable auto-opening on error, and create command aliases.
However, in its demo, I had issues with it displaying &lt;code&gt;document.querySelect('body')&lt;/code&gt;. All it showed was an empty object &lt;code&gt;{}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/liriliri/eruda"&gt;Eruda&lt;/a&gt; is a console with a size of 130k gzipped. However, with that size, you get an element browser, network monitor, and other tools you know from your desktop browser's DevTools. It also has a plugin ecosystem. It properly handles console.group as a toggle. Also you can submit multi-line scripts. I think the only thing Eruda is missing is a debugger 8-).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using hnldesign's MobileBrowser I was able to figure out why my javascript was crashing. If your hardware can handle it though, Eruda has many more features to make debugging on mobile device easier. Now the only time I need to find a USB cable is when I have to debug using breakpoints.&lt;/p&gt;

&lt;p&gt;Do you have recommendations for other tools or techniques for debugging on mobile devices? If so leave them in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Can a Details Element be Used as an Accessible Popup (Without JavaScript)?</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Sun, 05 Mar 2023 17:30:00 +0000</pubDate>
      <link>https://forem.com/rouilj/can-a-details-element-be-used-as-an-accessible-popup-without-javascript-1g61</link>
      <guid>https://forem.com/rouilj/can-a-details-element-be-used-as-an-accessible-popup-without-javascript-1g61</guid>
      <description>&lt;p&gt;I tried a little experiment to make a popup using a details element. The goal was to see if I could make a clickable popup without using JavaScript. It should be accessible for both sighted users and users using a screen reader like &lt;a href="https://www.nvaccess.org/" rel="noopener noreferrer"&gt;NVDA&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The HTML test case is a form with an info icon (the letter I in a circular blue field) after an input element. It is straightforward semantic HTML. The details element has a summary as its first element and a div enclosing paragraphs as the details' body. The summary element is the letter &lt;code&gt;I&lt;/code&gt; styled using CSS to put the &lt;code&gt;I&lt;/code&gt; (for info) on a circular lightblue field. The form looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8da255vyr93gkoivp3ak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8da255vyr93gkoivp3ak.png" alt="Image showing detail popup closed. The circular icon with an I in it used to open the popup has a light blue background color indicating it is closed. The rest of the screen displays labels, input fields, and one button at the bottom as you would see in a typical form." width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The HTML is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;A label:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;details&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;summary&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Activate for info on A label."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;I&lt;span class="c"&gt;&amp;lt;!--&amp;lt;img src="logo.png"&amp;gt;--&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
        Here is a popup/popover with some interesting information
        about the &lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;A label&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt; input field. It might also be very
        boring. The choice is up to you. But I would make it
        interesting if I were you.
          &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;&lt;/span&gt;Use space to close.&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"b"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;B label:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"b"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"c"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;C label:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"c"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
...
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a small sprinkling of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA" rel="noopener noreferrer"&gt;ARIA&lt;/a&gt;. The summary field's &lt;code&gt;aria-label&lt;/code&gt; announces what happens when you open the details element. Originally I added &lt;code&gt;aria-live="polite"&lt;/code&gt; on the &lt;code&gt;div&lt;/code&gt; after the &lt;code&gt;summary&lt;/code&gt;. But the detailed text was not announced when the details element opened. Moving &lt;code&gt;aria-live&lt;/code&gt; to the details element, announced the text automatically when opened.&lt;/p&gt;

&lt;p&gt;The summary icon trigger is opened by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clicking on the icon with the mouse,&lt;/li&gt;
&lt;li&gt;focusing on the icon (using tab) and

&lt;ul&gt;
&lt;li&gt;hitting space or&lt;/li&gt;
&lt;li&gt;hitting return&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Unlike other popups, this is triggered by a focusing user action rather than hover so it works on mobile as well.&lt;/p&gt;

&lt;p&gt;The CSS was also rather straightforward. Changing the detail element's &lt;code&gt;display&lt;/code&gt; to &lt;code&gt;inline-block&lt;/code&gt; (default &lt;code&gt;block&lt;/code&gt;) keeps it on the same line as the input. Changing the summary item's &lt;code&gt;display&lt;/code&gt; to &lt;code&gt;inline-block&lt;/code&gt; (default &lt;code&gt;list-item&lt;/code&gt;) removes the open/close arrow associated with the details summary.&lt;/p&gt;

&lt;p&gt;The icon was formed by using &lt;code&gt;border-radius&lt;/code&gt; and &lt;code&gt;background-color&lt;/code&gt;. To get things to align, I set the custom property &lt;code&gt;--icon-size&lt;/code&gt; to the line height of the page. This property then gets used to square up (circle up 8-)) the shape of the summary text icon. You can use an image as well if you want. The background of the icon changes when opened, and can be seen through transparent parts of an image.&lt;/p&gt;

&lt;p&gt;The details element (with the &lt;code&gt;inline&lt;/code&gt; class) has its &lt;code&gt;position&lt;/code&gt; set to &lt;code&gt;relative&lt;/code&gt;. This makes positioning the div containing the detailed text easier. The div itself uses &lt;code&gt;position: absolute&lt;/code&gt;. With these settings, the attributes &lt;a href="https://css-tricks.com/almanac/properties/t/top-right-bottom-left/" rel="noopener noreferrer"&gt;&lt;code&gt;left&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, and &lt;code&gt;bottom&lt;/code&gt;&lt;/a&gt; can be used to shift the div relative to its default location.&lt;/p&gt;

&lt;p&gt;This is the complete CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* styles for details popup */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--icon-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.5em&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* line height */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nc"&gt;.inline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* anchor popup position */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nc"&gt;.inline&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;lightblue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="no"&gt;black&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* remove marker */&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--icon-size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* make icon round */&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--icon-size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* make icon round */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nc"&gt;.inline&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* if you want to use an image */&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--icon-size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--icon-size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nc"&gt;.inline&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;margin-block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;padding-inline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nc"&gt;.inline&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nc"&gt;.inline&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt; &lt;span class="no"&gt;black&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-10ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* shift box to left */&lt;/span&gt;
  &lt;span class="c"&gt;/* Move the popup box after the icon with
     a 5px gap */&lt;/span&gt;
  &lt;span class="c"&gt;/* left: calc(var(--icon-size) + 5px); */&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* To align first line of popup with the baseline of the icon:
   * shift up by 1/3 of the line height (--icon-size * -0.3333)
   * then shift down by the margin-block on the div (+ 0.25em).
   * This turns out to be 0.25em. */&lt;/span&gt;
  &lt;span class="c"&gt;/*top: calc((var(--icon-size) * -0.333) + 0.25em);*/&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The open popup looks like this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqykbxskdtfy142vxml3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqykbxskdtfy142vxml3.png" alt="Image showing detail popup open. The circular icon with an I in it used to open/close the popup has a yellow background color indicating it is open. The popup sits on top of other text on the page. It is shifted to the left under the opening icon." width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prettier styling is an exercise for the reader 8-).&lt;/p&gt;

&lt;p&gt;It can be enhanced with JavaScript to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;close with a keypress (using the escape key for example)&lt;/li&gt;
&lt;li&gt;close with a mouse click outside of the popup&lt;/li&gt;
&lt;li&gt;(see: &lt;a href="https://github.com/zachleat/details-utils" rel="noopener noreferrer"&gt;details-utils&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even without JavaScript, it is usable in Firefox and Chrome (which includes browsers like Edge, Brave, Vivaldi...).  The details element should be accessible out of the box since it is a native HTML control. Although I only tested with NVDA and Windows Narrator, I expect it will work with other screen readers as well. (Note that Narrator would sometimes refuse to read the popup when opened. I have not discovered what causes it to refuse to read the popup on open. Moving the cursor with the right arrow key causes it to read the popup, and pressing the space key still closes the popup so....)&lt;/p&gt;

&lt;p&gt;This seems to be a successful experiment. Custom variables, details element, and calc are all well supported. This technique should work on the majority of browsers out there today.&lt;/p&gt;

&lt;p&gt;This technique will probably make it into some of the trackers I design for the &lt;a href="https://www.roundup-tracker.org/?ref=dev.to"&gt;Roundup Issue Tracker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Leave your quips, comments, evasions, questions, or answers below.&lt;/p&gt;

</description>
      <category>html</category>
      <category>webdev</category>
      <category>css</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Implementing a Command Palette and Task Timer (part 2)</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Sun, 19 Feb 2023 22:30:00 +0000</pubDate>
      <link>https://forem.com/rouilj/implementing-a-command-palette-and-task-timer-part-2-543</link>
      <guid>https://forem.com/rouilj/implementing-a-command-palette-and-task-timer-part-2-543</guid>
      <description>&lt;p&gt;I am a developer for the open source &lt;a href="https://www.roundup-tracker.org?ref=dev_to" rel="noopener noreferrer"&gt;Roundup Issue Tracker&lt;/a&gt;. It has many use cases. One is to develop issue trackers like GitHub Issues, Bugzilla, or Request Tracker. I also develop a custom issue tracker for a help desk environment. This article continues with the steps to add a task-timing feature for that tracker.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/rouilj/implementing-a-command-palette-and-task-timer-part-1-12j7"&gt;part 1 of this series&lt;/a&gt; I had just finished installing command-pal. Let's take a closer look at command-pal before we get to the timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhancing command-pal and Handling a Showstopper
&lt;/h2&gt;

&lt;p&gt;The  &lt;a href="https://blog.superhuman.com/how-to-build-a-remarkable-command-palette/" rel="noopener noreferrer"&gt;Superhuman blog post&lt;/a&gt;  lists other desirable features for a command palette:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fuzzy search&lt;/strong&gt; (for mispelings 8-)) - is included in command-pal using &lt;a href="https://fusejs.io/" rel="noopener noreferrer"&gt;fuse.js&lt;/a&gt;. (It looks like there is a &lt;a href="https://www.npmjs.com/package/@deepdub/ninja-keys" rel="noopener noreferrer"&gt;fork of Ninja Keys that has fuzzy search support&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;icons&lt;/strong&gt; - I created an &lt;a href="https://github.com/benwinding/command-pal/issues/22" rel="noopener noreferrer"&gt;issue&lt;/a&gt; and &lt;a href="https://github.com/benwinding/command-pal/pull/23" rel="noopener noreferrer"&gt;pull request&lt;/a&gt; to add support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;synonyms&lt;/strong&gt; - the fuzzy search includes the description field. This helps broaden the matching terms. But a description shouldn't be a keyword/synonym list. fuse.js can search an array of strings that are part of an object. Adding this functionality is a &lt;a href="https://github.com/benwinding/command-pal/issues/8" rel="noopener noreferrer"&gt;work in progress&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One interesting possibility is supporting multiple command palettes on a page. Each palette would have a different set of commands.  I am not sure that's a good idea. Superhuman suggests making the command palette omnipotent. Multiple palettes force the user to make a decision about which palette to activate. This breaks the idea of &lt;a href="https://www.interaction-design.org/literature/article/don-t-make-me-think-key-learning-points-for-ux-design-for-the-web" rel="noopener noreferrer"&gt;"don't make me think"&lt;/a&gt;.  I was able to create and activate multiple palettes with different hotkeys. However, more work on &lt;a href="https://github.com/benwinding/command-pal/issues/24" rel="noopener noreferrer"&gt;supporting multiple palettes on a page&lt;/a&gt; is needed.&lt;/p&gt;

&lt;p&gt;At its core, a command palette is a large select modal. Having the ability to &lt;a href="https://github.com/benwinding/command-pal/pull/28" rel="noopener noreferrer"&gt;activate the modal from Javascript&lt;/a&gt; could allow the palette to be used in more places without making the user think.&lt;/p&gt;

&lt;p&gt;I am pleased with command-pal. I have found it quite hackable even though I have never used Svelte before. However, I did have one potential showstopper.&lt;/p&gt;

&lt;p&gt;The tracker uses a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" rel="noopener noreferrer"&gt;Content Security Policy (CSP)&lt;/a&gt;. Style blocks in the page include a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce" rel="noopener noreferrer"&gt;nonce&lt;/a&gt;. If the nonce is missing or doesn't match the one in the CSP the style blocks are ignored. Svelte generates style blocks for each element that it creates/injects. These client-side blocks, don't have access to the server's CSP nonce. If they did have access to the nonce, the nonce would be useless for securing the page's assets. If the style blocks were in a file that could be fetched using a stylesheet link, everything would be fine. However, &lt;a href="https://github.com/benwinding/command-pal/issues/13#issuecomment-1416721301" rel="noopener noreferrer"&gt;efforts to do this&lt;/a&gt; with Svelt have failed. Another alternative is to generate a &lt;a href="https://content-security-policy.com/hash/" rel="noopener noreferrer"&gt;secure hash (sha256, 384, or 512)&lt;/a&gt;. How to get this generated at build time is unclear. However, &lt;a href="https://github.com/benwinding/command-pal/issues/13" rel="noopener noreferrer"&gt;I did find a way&lt;/a&gt; to calculate it at runtime that seems to work. I &lt;a href="https://github.com/benwinding/command-pal/pull/21" rel="noopener noreferrer"&gt;proposed a patch to allow an administrator to generate the hashes&lt;/a&gt; using the command-pal library.&lt;/p&gt;

&lt;p&gt;The mechanism for controlling the task timer is done. Now to turn my attention to actually timing tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Time All The Things
&lt;/h2&gt;

&lt;p&gt;I chose the &lt;a href="http://albert-gonzalez.github.io/easytimer.js/" rel="noopener noreferrer"&gt;easytimer.js&lt;/a&gt; library. It supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setting an initial value to start counting time&lt;/li&gt;
&lt;li&gt;one minute timer granularity - to reduce CPU load&lt;/li&gt;
&lt;li&gt;pausing and restarting timers while keeping their accumulated time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command palette allows the gross controls: stop/start/pause/resume. I still need to handle the other parts of the UI. The existing issue page provides a field for manually entering the task time. Rather than trying to create a new UI for the timer, I reused the existing field. The UI is relatively simple. There is a "Time spent" input element referred to as the "time element" below. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the user prefills the time element with a number of minutes, the timer will start counting up from that time. This is helpful if you forgot to start the timer and start it after say 10 minutes.&lt;/li&gt;
&lt;li&gt;The use case only requires 1 minute precision. Since I am counting in minutes, I add one minute to the start time. This rounds the time up to the next minute.&lt;/li&gt;
&lt;li&gt;The time element is updated only once a minute. This is great for reducing CPU use, but poor for user feedback. There needs to be some way to notify the user that the timer is running. This needs to work without interfering with the ability to use the rest of the issue interface. Using a popup could work. But popups clutter the interface. If it can't be moved, it may hide something the user wants to use.
Instead, I cycle the background color for the time element from yellow to goldenrod every 5 seconds. This is done using CSS rather than javascript. It should perform better than updating the input with a flashing indicator every second.&lt;/li&gt;
&lt;li&gt;The animation stops when the timer is paused. But the yellow background color is still shown in the time element.&lt;/li&gt;
&lt;li&gt;When the timer stops, the background of the time element returns to white.&lt;/li&gt;
&lt;li&gt;Besides the time element displaying state, other elements of the page change as well. Starting the timer makes a pending change to the issue. The issue page already has a mechanism for indicating a page with a pending change. Starting the timer triggers this mechanism. This results in:

&lt;ul&gt;
&lt;li&gt;a change in the background color of the time element label ("Time spent")&lt;/li&gt;
&lt;li&gt;a change in the background color for the H1 header on the page&lt;/li&gt;
&lt;li&gt;the H1 header on the page gets "pending changes" appended&lt;/li&gt;
&lt;li&gt;the title for the page has an exclamation mark prepended to it; allowing the page to be identified in a list of page titles&lt;/li&gt;
&lt;li&gt;the favicon for the page is overlayed with an exclamation mark inside a yellow dot; allowing the tab to be identified if the text can't be shown.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The command-pal Javascript Entries
&lt;/h3&gt;

&lt;p&gt;Here are the three commands for working with the timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// more commands&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Start or Unpause Timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue.item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// only show timers on an issue page&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// create the object&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;easytimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;minutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timeField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unable to find 'Time Spent' field. Are you viewing an issue?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Timer is running for: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
        &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeValues&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// restart timer&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isPaused&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Timer restarted at: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
        &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeValues&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// start timer instance&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;timeValue&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timeValue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// round up time to next minute: 10 seconds -&amp;gt; 1 minute&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;startValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeValue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;startValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt;
    &lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTotalTimeValues&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Timer started at: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeValues&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;animate_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTotalTimeValues&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// mark field/page with a pending change&lt;/span&gt;
    &lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;span class="icon"&amp;gt;&amp;amp;nbsp;⏱️ &amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// stopwatch emoji&lt;/span&gt;
  &lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// sort this to the top of the list&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pause Timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue.item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Update time field and snooze the timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No timer was started.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Timer is not running, time is: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeValues&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTotalTimeValues&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Timer paused at: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeValues&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stop Timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue.item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No timer was started.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isPaused&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Timer is not running, time is: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeValues&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeField&lt;/span&gt;

    &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// stop time update&lt;/span&gt;
    &lt;span class="nx"&gt;timeField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTotalTimeValues&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// also zero's timer.&lt;/span&gt;
    &lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="c1"&gt;// more commands&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Note: the version of command-pal that I am running has the change to support icons.)&lt;/p&gt;

&lt;p&gt;There is one helper function to set up the animation for the "Time spent" input field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;animate_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yellow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;goldenrod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yellow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working Example
&lt;/h2&gt;

&lt;p&gt;You can see this in action on the &lt;a href="https://rouilj.dynamic-dns.net/dev_to_demo/issue34" rel="noopener noreferrer"&gt;demo site&lt;/a&gt;. Use demo/demo for login. Activate the palette using &lt;code&gt;ctrl+space&lt;/code&gt;. The &lt;a href="https://rouilj.dynamic-dns.net/fossil/roundup_sysadmin/timeline?r=command_palette_and_timer&amp;amp;c=2023-02-02+17%3A34%3A05&amp;amp;ref=dev_to" rel="noopener noreferrer"&gt;code is also published&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>offers</category>
    </item>
    <item>
      <title>Implementing a Command Palette and Task Timer (part 1)</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Sun, 05 Feb 2023 21:09:25 +0000</pubDate>
      <link>https://forem.com/rouilj/implementing-a-command-palette-and-task-timer-part-1-12j7</link>
      <guid>https://forem.com/rouilj/implementing-a-command-palette-and-task-timer-part-1-12j7</guid>
      <description>&lt;p&gt;I am a developer for the open source &lt;a href="https://www.roundup-tracker.org" rel="noopener noreferrer"&gt;Roundup Issue Tracker&lt;/a&gt;. It has many use cases. One is to develop issue trackers like GitHub Issues, Bugzilla, or Request Tracker. I also develop a custom issue tracker for a help desk environment. This article describes the steps in adding a task-timing feature for that tracker.&lt;/p&gt;

&lt;p&gt;A user requested a task timer. The workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open an issue page,&lt;/li&gt;
&lt;li&gt;start a timer,&lt;/li&gt;
&lt;li&gt;start the task associated with the issue,&lt;/li&gt;
&lt;li&gt;use the issue page to document the work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When done, the user would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stop the timer,&lt;/li&gt;
&lt;li&gt;finish documentation, attach files, and make other changes to the issue,&lt;/li&gt;
&lt;li&gt;submit the time and other changes. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few decisions to make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timer functions

&lt;ul&gt;
&lt;li&gt;is start/stop enough?&lt;/li&gt;
&lt;li&gt;is pause/restart needed?&lt;/li&gt;
&lt;li&gt;does the user need to change/set/edit the timer?&lt;/li&gt;
&lt;li&gt;do we need to track seconds, minutes, and hours? Since the customer is billed for the time, recording seconds seems excessive. Is there a scenario where the timer would need to record seconds?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;timer controls

&lt;ul&gt;
&lt;li&gt;do we use a button/buttons? Does checking a checkbox activate the timer?&lt;/li&gt;
&lt;li&gt;what does the UI look like for each timer function?
Are the controls in the issue page?
Are the controls in a floating popup? Does the popup need to be movable/collapsible
so it doesn't block access to the underlying issue?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;notification feedback

&lt;ul&gt;
&lt;li&gt;how is the user notified that the timer is running/paused/stopped?&lt;/li&gt;
&lt;li&gt;how does the user see the elapsed time?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;future planning

&lt;ul&gt;
&lt;li&gt;how to add controls without cluttering an already complex interface&lt;/li&gt;
&lt;li&gt;what impact will this have on adding future feature requests in the same context&lt;/li&gt;
&lt;li&gt;does this guide us in implementing future workflows&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Evaluating a Command Palette
&lt;/h2&gt;

&lt;p&gt;I decided to try to install a &lt;a href="https://www.commandbar.com/blog/command-palette-past-present-and-future" rel="noopener noreferrer"&gt;command palette&lt;/a&gt;. A command palette is a UI interface usually activated by a hotkey. It allows searching and selecting from a context-sensitive list of commands. You might be familiar with the &lt;a href="https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette" rel="noopener noreferrer"&gt;VS Code command palette&lt;/a&gt;. Command palettes have many advantages for users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;discover useful commands (and their shortcuts)&lt;/li&gt;
&lt;li&gt;faster than scrolling down a long list of commands&lt;/li&gt;
&lt;li&gt;keyboard is faster than using the mouse&lt;/li&gt;
&lt;li&gt;invisible until activated&lt;/li&gt;
&lt;li&gt;make commands available that wouldn't be important enough to get a button or other UI element&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are many command palette implementations in JavaScript. Some are used with specific frameworks. For example, &lt;a href="https://github.com/timc1/kbar" rel="noopener noreferrer"&gt;kbar&lt;/a&gt; is a React component and &lt;a href="https://github.com/livewire-ui/spotlight" rel="noopener noreferrer"&gt;spotlight&lt;/a&gt; is a Laravel component. I wanted one that would work with &lt;a href="https://www.javatpoint.com/what-is-vanilla-javascript" rel="noopener noreferrer"&gt;Vanilla JavaScript&lt;/a&gt;. I identified two candidates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/benwinding/command-pal" rel="noopener noreferrer"&gt;command-pal&lt;/a&gt; - "The hackable command palette for the web, inspired by Visual Studio Code."&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ssleptsov/ninja-keys" rel="noopener noreferrer"&gt;Ninja Keys&lt;/a&gt; - "Keyboard shortcut interface for your website that works with Vanilla JS, Vue, and React."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ninja Keys uses &lt;a href="https://lit.dev/" rel="noopener noreferrer"&gt;Lit&lt;/a&gt; while command-pal uses &lt;a href="https://svelte.dev" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt;. I don't have any experience with either, so.... Both of them can bind any command on the palette to a hotkey thanks to &lt;a href="https://github.com/jaywcjlove/hotkeys/" rel="noopener noreferrer"&gt;hotkeys.js&lt;/a&gt;. Both are MIT licensed. Command-pal is larger in size, but it also bundles all the libraries it needs. It looks like Ninja Keys loads libraries/modules on demand from CDNs on the internet. Being able to use the library without internet access is a nice feature.&lt;/p&gt;

&lt;p&gt;Command-pal promotes itself as "hackable". This usually means flexibility and sometimes simplicity. I like both. Command-pal's feature set wasn't as impressive as Ninja Keys. But it does include a floating button to trigger the command palette on mobile. This is a nice touch as moving the page to access UI elements can be tedious on mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding command-pal
&lt;/h2&gt;

&lt;p&gt;As a result, I chose command-pal. Integrating it was easy. I downloaded the file from the &lt;a href="https://cdn.jsdelivr.net/npm/command-pal" rel="noopener noreferrer"&gt;CDN&lt;/a&gt;. I also downloaded the &lt;a href="https://github.com/benwinding/command-pal/tree/master/public" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt; from GitHub. I added a script tag and stylesheet link to the top-level page Roundup template file. This makes command-pal available on all the tracker's pages.&lt;/p&gt;

&lt;p&gt;To invoke it, I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CommandPal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
   &lt;span class="na"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ctrl+space&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;hotkeysGlobal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;inside a &lt;code&gt;script&lt;/code&gt; tag. The tracker is a web application. Sadly, the classic command palette hotkeys: "ctrl+k" or "ctrl+shift+p" are already used by the browser. I also wanted to activate the palette using the hotkey when focused on an input, select, or textarea. &lt;code&gt;hotkeysGlobal: true&lt;/code&gt; should do that, but it didn't work for me in Firefox, Chrome, or Brave). I &lt;a href="https://github.com/benwinding/command-pal/pull/18" rel="noopener noreferrer"&gt;submitted a pull request to fix it.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The commands array included:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [
    {
      name: "Exit Command Palette",
      contexts: [ "all" ],
      weight: -10,
    },
    {
      name: "Initial Screen",
      description: "Screen shown after login.",
      contexts: [ "all" ],
      handler: () =&amp;gt; (window.location.href = "."),
      shortcut: "ctrl+i",_
      weight: 5,
    }, ...
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Besides the fields used by command-pal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name,&lt;/li&gt;
&lt;li&gt;description,&lt;/li&gt;
&lt;li&gt;handler,&lt;/li&gt;
&lt;li&gt;shortcut&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added extra fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weight&lt;/li&gt;
&lt;li&gt;contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These were inspired by the &lt;a href="https://blog.superhuman.com/how-to-build-a-remarkable-command-palette/" rel="noopener noreferrer"&gt;Superhuman blog on building a remarkable command palette&lt;/a&gt;. Among the things they suggest are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order commands by popularity/utility&lt;/li&gt;
&lt;li&gt;listed commands are context sensitive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The commands are initially sorted by weight (using commands.sort()). This displays the most popular (highest weight) commands at the top of the menu. Displaying the &lt;a href="https://github.com/benwinding/command-pal/pull/30#issuecomment-1418245061" rel="noopener noreferrer"&gt;search results sorted by weight&lt;/a&gt; is an ongoing project.&lt;/p&gt;

&lt;p&gt;The command is shown if its &lt;code&gt;contexts&lt;/code&gt; property matches the current context. For example, the task timing commands only make sense when editing an issue. They should not be shown when viewing a list of issues, or a user's profile page. Inspecting the page's URL determines the current context. The code filters the command list, eliminating commands that are not appropriate for the context. Only then is CommandPal invoked.&lt;/p&gt;

&lt;p&gt;I hope you have enjoyed learning about command palettes and command-pal in particular. In part 2, we will use command-pal to control the task timer and look at how it integrates with the tracker built using the Roundup Issue Tracker.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
      &lt;div class="c-embed__cover"&gt;
        &lt;a href="https://www.roundup-tracker.org/" class="c-link s:max-w-50 align-middle" rel="noopener noreferrer"&gt;
          &lt;img alt="" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.roundup-tracker.org%2F_images%2Findex_logged_out.png" height="auto" class="m-0"&gt;
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;div class="c-embed__body"&gt;
      &lt;h2 class="fs-xl lh-tight"&gt;
        &lt;a href="https://www.roundup-tracker.org/" rel="noopener noreferrer" class="c-link"&gt;
          Roundup Issue Tracker - Roundup 2.4.0 documentation
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;p class="truncate-at-3"&gt;
          A simple-to-use and -install issue-tracking system with command-line, web, REST, XML-RPC and e-mail interfaces. Adaptable to many use cases. Allows you to customize the look and feel and implement different workflows.
        &lt;/p&gt;
      &lt;div class="color-secondary fs-s flex items-center"&gt;
        roundup-tracker.org
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Modern Form Update and Interior Border Hack with CSS Grid</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Mon, 28 Nov 2022 17:01:41 +0000</pubDate>
      <link>https://forem.com/rouilj/a-modern-form-update-and-interior-border-hack-with-css-grid-4kae</link>
      <guid>https://forem.com/rouilj/a-modern-form-update-and-interior-border-hack-with-css-grid-4kae</guid>
      <description>&lt;p&gt;I am a developer on the &lt;a href="https://www.roundup-tracker.org?ref=dev.to-modern_form_update"&gt;Roundup Issue Tracker&lt;/a&gt;. Roundup is a trouble ticketing system written in Python. It is very flexible. Developers, researchers, and support staff use Roundup to define and manage the lifecycle of issues.&lt;/p&gt;

&lt;p&gt;My background is in system administration. So I have built a tracker template designed for tracking system administration issues. Recently I redesigned the look of one of the more complex forms.&lt;/p&gt;

&lt;p&gt;This is the (short) story of the decisions I made and the techniques I used.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;I have limited design experience. When I set this up many years ago, it looked ok to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AivtarE7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s77umppmq50ebupd7xih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AivtarE7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s77umppmq50ebupd7xih.png" alt="A web form showing multiple fields (title, priority, status, etc.). Some span the width of the page, some are inside one of two columns. Fieldsets group the fields. Collapsed fieldsets show only their headers. The form has bold and bright-colored section headers. The expanded fieldsets have grooved borders." title="Form before changes." width="880" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at it today, the bold color swatches behind the fieldset legends and grooved borders make it seem cluttered and busy. It needs a more modern look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - fewer borders
&lt;/h2&gt;

&lt;p&gt;First I removed the borders from the fieldsets. This brought up an issue. Tabbing through the form moves you down the columns (divs) and not across the rows. The borders and the gap between them helped re-enforce this.&lt;/p&gt;

&lt;p&gt;I decided to add a vertical rule between the columns. I set &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/border-inline-end"&gt;border-inline-end&lt;/a&gt;  to 2 pixels (2px) on the first column. When the first column was longer than the second this looked great. But, the fieldset panes in the first column are collapsible. When the first column was shorter than the second column, the line between the columns didn't reach the bottom of the second column.&lt;/p&gt;

&lt;p&gt;The solution I came up with was to add a 2px border-inline-start on the second column. Then I moved the second column back by 2px (the width of the line) using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/margin-inline-start"&gt;margin-inline-start&lt;/a&gt;. These overlapped border lines always span the entire height of the longest column.&lt;/p&gt;

&lt;p&gt;The layout uses the &lt;a href="http://gridiculo.us/"&gt;Gridiculous&lt;/a&gt; grid system rather than &lt;code&gt;display: grid&lt;/code&gt;. This requires one last tweak. On narrower screens (e.g. a cell phone), the two side-by-side columns stack. Adding an @media query to remove the borders when on a narrow display completed the tweak.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to make a vertical rule with css grid
&lt;/h2&gt;

&lt;p&gt;I plan to use &lt;code&gt;display: grid&lt;/code&gt; at some point for this layout. It would reduce the amount of CSS I need to serve and provide a more flexible layout. But, there is no mechanism for creating a rule in the gaps of a grid layout. I found a trick for doing that, but I haven't committed to using it yet. There is one nice advantage to using this trick. When the columns stack, grid removes the gap and also the (pseudo) vertical rule. No need for an @media cleanup.&lt;/p&gt;

&lt;p&gt;To create the rule, I created a div (the base div) with the background-color set to the vertical rule's color. I set the CSS for that div to  &lt;code&gt;display: grid&lt;/code&gt; and the &lt;code&gt;gap: 0 2px&lt;/code&gt;. I placed two divs inside the base div. They form the columns. Set their background-color to that of the rest of the page. Now the 2px column gap in the grid exposes the darker rule color of the base div. You can only emulate a solid border, but it works for my use case. Could linear gradients emulate a grooved, ridged, dotted, or dashed border?&lt;/p&gt;

&lt;h2&gt;
  
  
  My eyes, my eyes
&lt;/h2&gt;

&lt;p&gt;The second change was to remove the banners of color behind the fieldset legends. With the more colorful themes, it's a bit bright on the eyes 8-). A simple underline matching the monochromatic theme replaces it. The HTML for the legend is &lt;code&gt;&amp;lt;legend&amp;gt;&amp;lt;span&amp;gt;Title Goes Here&amp;lt;/span&amp;gt;&amp;lt;/legend&amp;gt;&lt;/code&gt;.  Removing the background color from &lt;code&gt;legend &amp;gt; span&lt;/code&gt; cleared the banner. Since the span is set to use &lt;code&gt;display: block&lt;/code&gt;, adding a 2px &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/border-block-end"&gt;border-block-end&lt;/a&gt; completed the work. &lt;/p&gt;

&lt;h2&gt;
  
  
  The final look
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tKbzy760--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymsg4q8cxnvs06eljw6s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tKbzy760--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymsg4q8cxnvs06eljw6s.png" alt="The final redesign with  a form like the one above except with the changes described in the article." title="Form after the changes in this article." width="880" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a significant improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  But it's still (a) bad form...
&lt;/h2&gt;

&lt;p&gt;One question that you may ask is why tab order moves down the first/left column rather than across the form. As forms go this breaks a few rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;use a single column for forms. In this case, I have
 not only two primary columns, but the basic 
 fieldset has 2 subcolumns.&lt;/li&gt;
&lt;li&gt;put labels above their control, or right-aligned
 labels to the left of the control (for ltr
 languages). Here, I use left-aligned labels to the left
 of the controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To justify my decisions, let's look at the use cases. As with most trackers, there are two types of users of this tracker. The:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; people who need something done, and&lt;/li&gt;
&lt;li&gt; the people doing the work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also a third user, the person modifying the tracker for use at their site.&lt;/p&gt;

&lt;p&gt;The left-hand column provides information for the people who need something done. Tabbing down the column moves the page like there was only one column. Ideally, these users would scan down the left column of the page. Using left-aligned labels provides this scan line.&lt;/p&gt;

&lt;p&gt;Since Roundup can look at the user's role and remove/hide/collapse the second column we can get even closer to rule 1. This would make it a simpler form for these users. Still, displaying key status info above the fold requires packing a lot of fields in a small area. Following the "one column", "label on top" guidelines would increase scrolling.&lt;/p&gt;

&lt;p&gt;There is one more tweak for this redesign. With the second column displayed, a &lt;a href="https://webaim.org/techniques/skipnav/"&gt;skip link&lt;/a&gt;  will be added at the bottom of the first column. The skip link will jump to the update textarea where the user can add a change note/update. This bypasses all the tab stops in the second column.&lt;/p&gt;

&lt;p&gt;Adding a skip link at the top of the page to target the update  textarea area is an idea. However, a skip link already exists at the top of the page. It jumps to the main section, bypassing the left-hand navigation column (not shown in the images). Would adding a second top-of-page skip link be confusing clutter? I'm not sure.&lt;/p&gt;

&lt;p&gt;For people doing the work, references to the schedule and linked tickets are more useful. The second column provides access to them. The "All Fields" tab provides even more information hidden from view. For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;summaries of related tickets&lt;/li&gt;
&lt;li&gt;a graph of relationships among tickets&lt;/li&gt;
&lt;li&gt;extra fields for scheduling automatic actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This brings us to the third user. I tried to set up a form that the Roundup admin could adapt for their workflows. The admin should not have to be an HTML/CSS expert. The idea of Roundup is that it is customizable for many use cases. I have tried to use clean semantic HTML and simple CSS to support that idea.&lt;/p&gt;

&lt;p&gt;Thanks for reading this. I hope you enjoyed it and learned something. You can find more information on Roundup using the link below.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
      &lt;div class="c-embed__cover"&gt;
        &lt;a href="https://www.roundup-tracker.org/?ref=dev.to-modern_form_update" class="c-link s:max-w-50 align-middle" rel="noopener noreferrer"&gt;
          &lt;img alt="" src="https://res.cloudinary.com/practicaldev/image/fetch/s--Nm4RcPml--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.roundup-tracker.org/_images/index_logged_out.png" height="608" class="m-0" width="880"&gt;
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;div class="c-embed__body"&gt;
      &lt;h2 class="fs-xl lh-tight"&gt;
        &lt;a href="https://www.roundup-tracker.org/?ref=dev.to-modern_form_update" rel="noopener noreferrer" class="c-link"&gt;
          Roundup Issue Tracker - Roundup 2.2.0 documentation
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;p class="truncate-at-3"&gt;
          A simple-to-use and -install issue-tracking system with command-line, web, REST, XML-RPC and e-mail interfaces. Adaptable to many uses cases. Allows you to customise the look and feel and implement different workflows.
        &lt;/p&gt;
      &lt;div class="color-secondary fs-s flex items-center"&gt;
          &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://res.cloudinary.com/practicaldev/image/fetch/s--QGnQRU6q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.roundup-tracker.org/_static/favicon.ico" width="16" height="16"&gt;
        roundup-tracker.org
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>html</category>
      <category>css</category>
    </item>
    <item>
      <title>My Journey to Open-Source Development</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Fri, 29 Jul 2022 05:49:19 +0000</pubDate>
      <link>https://forem.com/rouilj/my-journey-to-open-source-development-1fff</link>
      <guid>https://forem.com/rouilj/my-journey-to-open-source-development-1fff</guid>
      <description>&lt;p&gt;Welcome to the first article on the ongoing development of the &lt;a href="https://www.roundup-tracker.org?ref=dev.to_1"&gt;Roundup Issue Tracker&lt;/a&gt;. Before we get started, a little about me.&lt;/p&gt;

&lt;p&gt;I have a B.S. in Physics which included some advanced computer science courses. But, my career ended up in system administration. I have &lt;a href="https://www.usenix.org/legacy/events/lisa09/training/tutonefile.html#s2"&gt;taught&lt;/a&gt; and published a &lt;a href="https://www.usenix.org/legacy/events/lisa04/tech/full_papers/rouillard/rouillard_html/index.html"&gt;few&lt;/a&gt; peer-reviewed &lt;a href="https://static.usenix.org/publications/library/proceedings/lisa94/rouillard.html"&gt;papers&lt;/a&gt; at conferences over the years. Trying to reduce toil and improve operations, lead to my interest in ticketing systems. Tools from LEAN and Six Sigma have proven valuable in analyzing ticketing and real-time logging data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues Need Tracking
&lt;/h2&gt;

&lt;p&gt;In 2002, I ran a lab that installed a system monitoring solution on Sun computers. We needed to track requests that came to the lab. I evaluated ticketing systems across a few dimensions. The &lt;a href="https://www.roundup-tracker.org?ref=dev.to_1"&gt;Roundup Issue Tracker&lt;/a&gt; scored the highest.&lt;/p&gt;

&lt;p&gt;Five minutes after downloading the software, I had a classic tracker running in demo mode. After entering a few tickets and adding a few keywords (tags), my co-worker and I decided it could work. It was flexible and looked like we could extend it to support our future growth. Little did I know that this was the start of a 20+ year relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roundup Basics
&lt;/h2&gt;

&lt;p&gt;Roundup tracker has two components: the Roundup core and the tracker instance. The core is written in Python. The tracker instance supports one of two HTML templating engines: (&lt;a href="https://pagetemplates.readthedocs.io/en/latest/introduction.html"&gt;Zope Page Templates&lt;/a&gt; or &lt;a href="https://palletsprojects.com/p/jinja/"&gt;Jinja2&lt;/a&gt;). It is programmed in Python.  The tracker instance controls the look/feel/functionality of the tracker. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defines the database schema/data model,&lt;/li&gt;
&lt;li&gt;provides the web interface (with customizable
 HTML and CSS),&lt;/li&gt;
&lt;li&gt;executes Python code to &lt;a href="https://www.roundup-tracker.org/docs/customizing.html?ref=dev.to_1#detectors-adding-behaviour-to-your-tracker"&gt;validate
 entered data and react&lt;/a&gt; to a change
 (e.g. by emailing notification messages),&lt;/li&gt;
&lt;li&gt;can &lt;a href="https://www.roundup-tracker.org/docs/customizing.html?ref=dev.to_1#interfaces-py-hooking-into-the-core-of-roundup"&gt;hook into the core code&lt;/a&gt; to control how the core operates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One instance of the Roundup core can run many trackers. Each tracker can have a very different presentation and function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting My Feet Wet
&lt;/h2&gt;

&lt;p&gt;When I started, Roundup was still very new. I started developing my customized tracker to learn Python and experiment with Roundup. My tracker implemented several ideas from &lt;a href="https://bestpractical.com/request-tracker/"&gt;Request Tracker (RT)&lt;/a&gt;. I developed the key components, &lt;a href="https://rt-wiki.bestpractical.com/wiki/ManualUsingWebInterface#Replying_and_commenting"&gt;replies/comments&lt;/a&gt;, &lt;a href="https://rt-wiki.bestpractical.com/wiki/ManualUsingWebInterface#Links"&gt;dependencies, and grouping (parent/child)&lt;/a&gt;, in less than 40 hours of effort over a few weeks. Roundup's implementation and documentation made building these significant features easy.&lt;/p&gt;

&lt;p&gt;The other advantage was that Richard Jones, the developer, was in Australia. We set up a "follow the sun" development cycle. I emailed him a problem when I went to bed. By the time I got up the next morning, he would have a fix ready for me to use when I got home after work. This fast feedback is something I try to maintain in the Roundup community.&lt;/p&gt;

&lt;p&gt;I never deployed my custom tracker, but the classic tracker worked well for the lab. As a result, the sales engineering group requested (and got) a tracker. After I left the company in 2005, they upgraded the Roundup core and converted the database to MySQL (from a dbm key/value store).&lt;/p&gt;

&lt;h2&gt;
  
  
  Back in a More Active Role
&lt;/h2&gt;

&lt;p&gt;My next IT job used Request Tracker. I followed Roundup development but didn't contribute much. In 2012, there was some interest at work in setting up a customer-facing portal based on Roundup. I started development on my tracker again and re-joined the Roundup community. Over the next few years, I developed code to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a transaction source property for data changes. Makes changes from the web interface but prevents the same change if it originated from (unsigned) email.&lt;/li&gt;
&lt;li&gt;the filter command to the &lt;code&gt;roundup-admin&lt;/code&gt; command line interface makes data searching easier.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nofollow&lt;/code&gt; relationships to links which makes Roundup less useful to spammers.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://owasp.org/www-community/attacks/csrf"&gt;CSRF&lt;/a&gt; 
protection using &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern"&gt;synchronizer tokens&lt;/a&gt; makes Roundup more secure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Releasing Roundup
&lt;/h2&gt;

&lt;p&gt;By 2018, Richard had stepped back, the last release was January 2016. I took over as a lead developer and release engineer. I released version 1.6.0 in July of 2018. This merged several existing patches, cleaning up &lt;a href="https://issues.roundup-tracker.org?ref=dev.to_1"&gt;Roundup's issue tracker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since 2018, there were two major changes added by other developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;support for both Python 2 and Python 3 (which required a 2.0.0 release)&lt;/li&gt;
&lt;li&gt;addition of a &lt;a href="https://www.roundup-tracker.org/docs/rest.html?ref=dev.to_1"&gt;REST interface&lt;/a&gt;
 beside the &lt;a href="https://www.roundup-tracker.org/docs/xmlrpc.html?ref=dev.to_1"&gt;XML-RPC&lt;/a&gt;
 interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On July 13th, 2022, I made my &lt;a href="https://sourceforge.net/p/roundup/mailman/roundup-users/thread/20210713043457.083CE6A0022%40pe15.cs.umb.edu/"&gt;fifth annual release&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since Roundup deployments are on-prem, I established a yearly release cadence. Other issue trackers have more frequent releases. But, many of those releases address UI or workflow issues. In Roundup these are implemented in the (customized) tracker instance. So the user is not dependent on the fixes pushed from upstream. Each release includes &lt;a href="https://www.roundup-tracker.org/docs/upgrading.html?ref=dev.to_1"&gt;upgrading instructions&lt;/a&gt;. These instructions can include changes for the tracker instance to bring it up to date.&lt;/p&gt;

&lt;p&gt;The slower release cadence has an advantage for the users. It reduces the need to update tracker instances to once a year rather than every few months. However, the yearly cadence results in less public visibility for Roundup.  &lt;/p&gt;

&lt;p&gt;I plan more articles on developing and maintaining an open-source application. Some future topics I am considering include (&lt;small&gt;in no particular order&lt;/small&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Customisability Trap&lt;/li&gt;
&lt;li&gt;Kitchen Sink Development&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Responsive Design&lt;/li&gt;
&lt;li&gt;The Problem with Selects&lt;/li&gt;
&lt;li&gt;What?? No Javascript!&lt;/li&gt;
&lt;li&gt;Docker All the Things&lt;/li&gt;
&lt;li&gt;Colors and Themes&lt;/li&gt;
&lt;li&gt;The Bus Factor&lt;/li&gt;
&lt;li&gt;Evaluating Tracking Software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://lwn.net/Articles/869118/"&gt;Roundup 20&lt;sup&gt;th&lt;/sup&gt; anniversary article on lwn.net&lt;/a&gt; includes more information on Roundup.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed this article. Thanks for reading.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to change details label when open/closed without JavaScript</title>
      <dc:creator>John P. Rouillard</dc:creator>
      <pubDate>Mon, 12 Jul 2021 21:26:26 +0000</pubDate>
      <link>https://forem.com/rouilj/how-to-change-details-label-when-open-closed-without-javascript-1n3c</link>
      <guid>https://forem.com/rouilj/how-to-change-details-label-when-open-closed-without-javascript-1n3c</guid>
      <description>&lt;p&gt;I wish the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details" rel="noopener noreferrer"&gt;details element&lt;/a&gt; would support different labels (provided by the summary tag) for its open and closed states.&lt;/p&gt;

&lt;p&gt;Consider a label of "Closed, click to open" when the details element is closed and "Open, click to close" when open:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6955uhrejxff5exayli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6955uhrejxff5exayli.png" alt="Open details element with label changed to indicate state."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sadly only one summary tag is supported by the details element. However you can use an empty summary with a ::before pseudo-element to implement this.&lt;/p&gt;

&lt;p&gt;Save the following to a file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    #widget1 div { padding-inline-start: 1em; max-width: 20em;}
    #widget1 summary::before { content: "Closed, click to open";}
    #widget1[open] summary::before { content: "Open, click to close";}
    #widget1[open] summary { height: 2em;}
  &amp;lt;/style&amp;gt;

  &amp;lt;p&amp;gt; Text before details.&amp;lt;/p&amp;gt;
  &amp;lt;details id="widget1"&amp;gt;
    &amp;lt;summary&amp;gt;&amp;lt;/summary&amp;gt;
    &amp;lt;div&amp;gt;
    A widget can be in one of two states. The default closed
    state displays only the triangle and the label inside summary
    (or a user agent-defined default string if no summary).
    &amp;lt;/div&amp;gt;
  &amp;lt;/details&amp;gt;
  &amp;lt;p&amp;gt;Text after details.&amp;lt;/p&amp;gt;
  &amp;lt;body&amp;gt;
&amp;lt;/html&amp;gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;to experiment.&lt;br&gt;
The second and third style lines are where the magic happens.&lt;/p&gt;

&lt;p&gt;Note that it might not be accessible. I didn't try to see if adding a live region aria label to the summary would cause the label change to be announced. I suspect it will not, but refining this is left as an exercise for the reader. Add your experimental findings to the comments below.&lt;/p&gt;

</description>
      <category>html</category>
      <category>css</category>
    </item>
  </channel>
</rss>
