mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 17:24:22 +00:00 
			
		
		
		
	Rework and fix stopwatch (#30732)
Fixes https://github.com/go-gitea/gitea/issues/30721 and overhauls the stopwatch. Time is now shown inside the "dot" icon and on both mobile and desktop. All rendering is now done by `<relative-time>`, the `pretty-ms` dependency is dropped. Desktop: <img width="557" alt="Screenshot 2024-04-29 at 22 33 27" src="https://github.com/go-gitea/gitea/assets/115237/3a46cdbf-6af2-4bf9-b07f-021348badaac"> Mobile: <img width="640" alt="Screenshot 2024-04-29 at 22 34 19" src="https://github.com/go-gitea/gitea/assets/115237/8a2beea7-bd5d-473f-8fff-66f63fd50877"> Note for tippy: Previously, tippy instances defaulted to "menu" theme, but that theme is really only meant for `.ui.menu`, so it was not optimal for the stopwatch popover. This introduces a unopinionated `default` theme that has no padding and should be suitable for all content. I reviewed all existing uses and explicitely set the desired `theme` on all of them.
This commit is contained in:
		
							
								
								
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -42,7 +42,6 @@
 | 
			
		||||
        "postcss": "8.4.38",
 | 
			
		||||
        "postcss-loader": "8.1.1",
 | 
			
		||||
        "postcss-nesting": "12.1.2",
 | 
			
		||||
        "pretty-ms": "9.0.0",
 | 
			
		||||
        "sortablejs": "1.15.2",
 | 
			
		||||
        "swagger-ui-dist": "5.17.2",
 | 
			
		||||
        "tailwindcss": "3.4.3",
 | 
			
		||||
@@ -9170,17 +9169,6 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/parse-ms": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/path-exists": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 | 
			
		||||
@@ -9772,20 +9760,6 @@
 | 
			
		||||
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/pretty-ms": {
 | 
			
		||||
      "version": "9.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "parse-ms": "^4.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/printable-characters": {
 | 
			
		||||
      "version": "1.0.42",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,6 @@
 | 
			
		||||
    "postcss": "8.4.38",
 | 
			
		||||
    "postcss-loader": "8.1.1",
 | 
			
		||||
    "postcss-nesting": "12.1.2",
 | 
			
		||||
    "pretty-ms": "9.0.0",
 | 
			
		||||
    "sortablejs": "1.15.2",
 | 
			
		||||
    "swagger-ui-dist": "5.17.2",
 | 
			
		||||
    "tailwindcss": "3.4.3",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,14 @@
 | 
			
		||||
 | 
			
		||||
		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
 | 
			
		||||
		<div class="ui secondary menu item navbar-mobile-right only-mobile">
 | 
			
		||||
			{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
 | 
			
		||||
			<a id="mobile-stopwatch-icon" class="active-stopwatch item tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
 | 
			
		||||
				<div class="tw-relative">
 | 
			
		||||
					{{svg "octicon-stopwatch"}}
 | 
			
		||||
					<span class="header-stopwatch-dot"></span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</a>
 | 
			
		||||
			{{end}}
 | 
			
		||||
			{{if .IsSigned}}
 | 
			
		||||
			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 | 
			
		||||
				<div class="tw-relative">
 | 
			
		||||
@@ -74,41 +82,13 @@
 | 
			
		||||
				</div><!-- end content avatar menu -->
 | 
			
		||||
			</div><!-- end dropdown avatar menu -->
 | 
			
		||||
		{{else if .IsSigned}}
 | 
			
		||||
			{{if EnableTimetracking}}
 | 
			
		||||
			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
 | 
			
		||||
			{{if and EnableTimetracking .ActiveStopwatch}}
 | 
			
		||||
			<a class="item not-mobile active-stopwatch tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
 | 
			
		||||
				<div class="tw-relative">
 | 
			
		||||
					{{svg "octicon-stopwatch"}}
 | 
			
		||||
					<span class="header-stopwatch-dot"></span>
 | 
			
		||||
				</div>
 | 
			
		||||
				<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
 | 
			
		||||
			</a>
 | 
			
		||||
			<div class="active-stopwatch-popup item tippy-target tw-p-2">
 | 
			
		||||
				<div class="tw-flex tw-items-center">
 | 
			
		||||
					<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
 | 
			
		||||
						{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
 | 
			
		||||
						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
 | 
			
		||||
						<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
 | 
			
		||||
							{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
 | 
			
		||||
						</span>
 | 
			
		||||
					</a>
 | 
			
		||||
					<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<button
 | 
			
		||||
							type="submit"
 | 
			
		||||
							class="ui button mini compact basic icon"
 | 
			
		||||
							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
 | 
			
		||||
						>{{svg "octicon-square-fill"}}</button>
 | 
			
		||||
					</form>
 | 
			
		||||
					<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<button
 | 
			
		||||
							type="submit"
 | 
			
		||||
							class="ui button mini compact basic icon"
 | 
			
		||||
							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
 | 
			
		||||
						>{{svg "octicon-trash"}}</button>
 | 
			
		||||
					</form>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
 | 
			
		||||
@@ -202,4 +182,33 @@
 | 
			
		||||
			</a>
 | 
			
		||||
		{{end}}
 | 
			
		||||
	</div><!-- end full right menu -->
 | 
			
		||||
 | 
			
		||||
	{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
 | 
			
		||||
		<div class="active-stopwatch-popup tippy-target">
 | 
			
		||||
			<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
 | 
			
		||||
				<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}">
 | 
			
		||||
					{{svg "octicon-issue-opened" 16}}
 | 
			
		||||
					<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
 | 
			
		||||
				</a>
 | 
			
		||||
				<div class="tw-flex tw-gap-1">
 | 
			
		||||
					<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<button
 | 
			
		||||
							type="submit"
 | 
			
		||||
							class="ui button mini compact basic icon tw-mr-0"
 | 
			
		||||
							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
 | 
			
		||||
						>{{svg "octicon-square-fill"}}</button>
 | 
			
		||||
					</form>
 | 
			
		||||
					<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<button
 | 
			
		||||
							type="submit"
 | 
			
		||||
							class="ui button mini compact basic icon tw-mr-0"
 | 
			
		||||
							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
 | 
			
		||||
						>{{svg "octicon-trash"}}</button>
 | 
			
		||||
					</form>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{{end}}
 | 
			
		||||
</nav>
 | 
			
		||||
 
 | 
			
		||||
@@ -103,19 +103,12 @@
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    min-height: 48px;
 | 
			
		||||
  }
 | 
			
		||||
  #navbar #mobile-stopwatch-icon,
 | 
			
		||||
  #navbar #mobile-notifications-icon {
 | 
			
		||||
    margin-right: 6px !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#navbar a.item .notification_count {
 | 
			
		||||
  color: var(--color-nav-bg);
 | 
			
		||||
  padding: 0 3.75px;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  line-height: 12px;
 | 
			
		||||
  font-weight: var(--font-weight-bold);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#navbar a.item:hover .notification_count,
 | 
			
		||||
#navbar a.item:hover .header-stopwatch-dot {
 | 
			
		||||
  border-color: var(--color-nav-hover-bg);
 | 
			
		||||
@@ -123,6 +116,11 @@
 | 
			
		||||
 | 
			
		||||
#navbar a.item .notification_count,
 | 
			
		||||
#navbar a.item .header-stopwatch-dot {
 | 
			
		||||
  color: var(--color-nav-bg);
 | 
			
		||||
  padding: 0 3.75px;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  line-height: 12px;
 | 
			
		||||
  font-weight: var(--font-weight-bold);
 | 
			
		||||
  background: var(--color-primary);
 | 
			
		||||
  border: 2px solid var(--color-nav-bg);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
@@ -135,6 +133,8 @@
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  z-index: 1; /* prevent menu button background from overlaying icon */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.secondary-nav {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,8 @@
 | 
			
		||||
 | 
			
		||||
.tippy-box {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background-color: var(--color-body);
 | 
			
		||||
  color: var(--color-secondary-dark-6);
 | 
			
		||||
  background-color: var(--color-menu);
 | 
			
		||||
  color: var(--color-text);
 | 
			
		||||
  border: 1px solid var(--color-secondary);
 | 
			
		||||
  border-radius: var(--border-radius);
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
@@ -25,7 +25,6 @@
 | 
			
		||||
 | 
			
		||||
.tippy-content {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding: 1rem; /* if you need different padding, use different data-theme */
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -166,5 +165,5 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tippy-svg-arrow-inner {
 | 
			
		||||
  fill: var(--color-body);
 | 
			
		||||
  fill: var(--color-menu);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ export function attachRefIssueContextPopup(refIssues) {
 | 
			
		||||
    if (!owner) return;
 | 
			
		||||
 | 
			
		||||
    const el = document.createElement('div');
 | 
			
		||||
    el.classList.add('tw-p-3');
 | 
			
		||||
    refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
 | 
			
		||||
 | 
			
		||||
    const view = createApp(ContextPopup);
 | 
			
		||||
@@ -30,6 +31,7 @@ export function attachRefIssueContextPopup(refIssues) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createTippy(refIssue, {
 | 
			
		||||
      theme: 'default',
 | 
			
		||||
      content: el,
 | 
			
		||||
      placement: 'top-start',
 | 
			
		||||
      interactive: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,7 @@ function showLineButton() {
 | 
			
		||||
  btn.closest('.code-view').append(menu.cloneNode(true));
 | 
			
		||||
 | 
			
		||||
  createTippy(btn, {
 | 
			
		||||
    theme: 'menu',
 | 
			
		||||
    trigger: 'click',
 | 
			
		||||
    hideOnClick: true,
 | 
			
		||||
    content: menu,
 | 
			
		||||
 
 | 
			
		||||
@@ -502,6 +502,7 @@ export function initRepoPullRequestReview() {
 | 
			
		||||
  if ($reviewBtn.length && $panel.length) {
 | 
			
		||||
    const tippy = createTippy($reviewBtn[0], {
 | 
			
		||||
      content: $panel[0],
 | 
			
		||||
      theme: 'default',
 | 
			
		||||
      placement: 'bottom',
 | 
			
		||||
      trigger: 'click',
 | 
			
		||||
      maxWidth: 'none',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import prettyMilliseconds from 'pretty-ms';
 | 
			
		||||
import {createTippy} from '../modules/tippy.js';
 | 
			
		||||
import {GET} from '../modules/fetch.js';
 | 
			
		||||
import {hideElem, showElem} from '../utils/dom.js';
 | 
			
		||||
@@ -10,28 +9,31 @@ export function initStopwatch() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const stopwatchEl = document.querySelector('.active-stopwatch-trigger');
 | 
			
		||||
  const stopwatchEls = document.querySelectorAll('.active-stopwatch');
 | 
			
		||||
  const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
 | 
			
		||||
 | 
			
		||||
  if (!stopwatchEl || !stopwatchPopup) {
 | 
			
		||||
  if (!stopwatchEls.length || !stopwatchPopup) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopwatchEl.removeAttribute('href'); // intended for noscript mode only
 | 
			
		||||
 | 
			
		||||
  createTippy(stopwatchEl, {
 | 
			
		||||
    content: stopwatchPopup,
 | 
			
		||||
    placement: 'bottom-end',
 | 
			
		||||
    trigger: 'click',
 | 
			
		||||
    maxWidth: 'none',
 | 
			
		||||
    interactive: true,
 | 
			
		||||
    hideOnClick: true,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
 | 
			
		||||
  const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
 | 
			
		||||
  if (currSeconds) {
 | 
			
		||||
    updateStopwatchTime(currSeconds);
 | 
			
		||||
  const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
 | 
			
		||||
  if (seconds) {
 | 
			
		||||
    updateStopwatchTime(parseInt(seconds));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const stopwatchEl of stopwatchEls) {
 | 
			
		||||
    stopwatchEl.removeAttribute('href'); // intended for noscript mode only
 | 
			
		||||
 | 
			
		||||
    createTippy(stopwatchEl, {
 | 
			
		||||
      content: stopwatchPopup.cloneNode(true),
 | 
			
		||||
      placement: 'bottom-end',
 | 
			
		||||
      trigger: 'click',
 | 
			
		||||
      maxWidth: 'none',
 | 
			
		||||
      interactive: true,
 | 
			
		||||
      hideOnClick: true,
 | 
			
		||||
      theme: 'default',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let usingPeriodicPoller = false;
 | 
			
		||||
@@ -124,10 +126,9 @@ async function updateStopwatch() {
 | 
			
		||||
 | 
			
		||||
function updateStopwatchData(data) {
 | 
			
		||||
  const watch = data[0];
 | 
			
		||||
  const btnEl = document.querySelector('.active-stopwatch-trigger');
 | 
			
		||||
  const btnEls = document.querySelectorAll('.active-stopwatch');
 | 
			
		||||
  if (!watch) {
 | 
			
		||||
    clearStopwatchTimer();
 | 
			
		||||
    hideElem(btnEl);
 | 
			
		||||
    hideElem(btnEls);
 | 
			
		||||
  } else {
 | 
			
		||||
    const {repo_owner_name, repo_name, issue_index, seconds} = watch;
 | 
			
		||||
    const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
 | 
			
		||||
@@ -137,31 +138,28 @@ function updateStopwatchData(data) {
 | 
			
		||||
    const stopwatchIssue = document.querySelector('.stopwatch-issue');
 | 
			
		||||
    if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
 | 
			
		||||
    updateStopwatchTime(seconds);
 | 
			
		||||
    showElem(btnEl);
 | 
			
		||||
    showElem(btnEls);
 | 
			
		||||
  }
 | 
			
		||||
  return Boolean(data.length);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let updateTimeIntervalId = null; // holds setInterval id when active
 | 
			
		||||
function clearStopwatchTimer() {
 | 
			
		||||
  if (updateTimeIntervalId !== null) {
 | 
			
		||||
    clearInterval(updateTimeIntervalId);
 | 
			
		||||
    updateTimeIntervalId = null;
 | 
			
		||||
// TODO: This flickers on page load, we could avoid this by making a custom
 | 
			
		||||
// element to render time periods. Feeding a datetime in backend does not work
 | 
			
		||||
// when time zone between server and client differs.
 | 
			
		||||
function updateStopwatchTime(seconds) {
 | 
			
		||||
  if (!Number.isFinite(seconds)) return;
 | 
			
		||||
  const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
 | 
			
		||||
  for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
 | 
			
		||||
    const existing = parent.querySelector(':scope > relative-time');
 | 
			
		||||
    if (existing) {
 | 
			
		||||
      existing.setAttribute('datetime', datetime);
 | 
			
		||||
    } else {
 | 
			
		||||
      const el = document.createElement('relative-time');
 | 
			
		||||
      el.setAttribute('format', 'micro');
 | 
			
		||||
      el.setAttribute('datetime', datetime);
 | 
			
		||||
      el.setAttribute('lang', 'en-US');
 | 
			
		||||
      el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
 | 
			
		||||
      parent.append(el);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
function updateStopwatchTime(seconds) {
 | 
			
		||||
  const secs = parseInt(seconds);
 | 
			
		||||
  if (!Number.isFinite(secs)) return;
 | 
			
		||||
 | 
			
		||||
  clearStopwatchTimer();
 | 
			
		||||
  const stopwatch = document.querySelector('.stopwatch-time');
 | 
			
		||||
  // TODO: replace with <relative-time> similar to how system status up time is shown
 | 
			
		||||
  const start = Date.now();
 | 
			
		||||
  const updateUi = () => {
 | 
			
		||||
    const delta = Date.now() - start;
 | 
			
		||||
    const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
 | 
			
		||||
    if (stopwatch) stopwatch.textContent = dur;
 | 
			
		||||
  };
 | 
			
		||||
  updateUi();
 | 
			
		||||
  updateTimeIntervalId = setInterval(updateUi, 1000);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -37,8 +37,10 @@ export function createTippy(target, opts = {}) {
 | 
			
		||||
      return onShow?.(instance);
 | 
			
		||||
    },
 | 
			
		||||
    arrow: arrow || (theme === 'bare' ? false : arrowSvg),
 | 
			
		||||
    role: role || 'menu', // HTML role attribute
 | 
			
		||||
    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
 | 
			
		||||
    // HTML role attribute, ideally the default role would be "popover" but it does not exist
 | 
			
		||||
    role: role || 'menu',
 | 
			
		||||
    // CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
 | 
			
		||||
    theme: theme || role || 'default',
 | 
			
		||||
    plugins: [followCursor],
 | 
			
		||||
    ...other,
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -131,6 +131,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
 | 
			
		||||
      interactive: true,
 | 
			
		||||
      placement: 'bottom-end',
 | 
			
		||||
      role: 'menu',
 | 
			
		||||
      theme: 'menu',
 | 
			
		||||
      content: this.tippyContent,
 | 
			
		||||
      onShow: () => { // FIXME: onShown doesn't work (never be called)
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user