diff --git a/README.md b/README.md index 5ec6bc9..75565c5 100644 --- a/README.md +++ b/README.md @@ -8,53 +8,30 @@ Demo: ## Get started -### Recommended IDE Setup - -[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). - -### Recommended Browser Setup - -- Chromium-based browsers (Chrome, Edge, Brave, etc.): - - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) - - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) -- Firefox: - - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) - - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) - -### Type Support for `.vue` Imports in TS - -TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. - -### Customize configuration - -See [Vite Configuration Reference](https://vite.dev/config/). - -### Project Setup - -```sh +```bash pnpm install ``` -#### Compile and Hot-Reload for Development +### Compile and Hot-Reload for Development -```sh +```bash pnpm dev ``` -#### Type-Check, Compile and Minify for Production +### Type-Check, Compile and Minify for Production -```sh +```bash pnpm build ``` -#### Run Unit Tests with [Vitest](https://vitest.dev/) +### Run Unit Tests with [Vitest](https://vitest.dev/) -```sh +```bash pnpm test:unit ``` -#### Lint with [ESLint](https://eslint.org/) +### Lint with [ESLint](https://eslint.org/) -```sh +```bash pnpm lint ``` diff --git a/package.json b/package.json index 8f628f2..24506f3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "format": "oxfmt src/" }, "dependencies": { + "@phosphor-icons/vue": "^2.2.1", "vue": "beta" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a154dfa..d8e298d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: .: dependencies: + '@phosphor-icons/vue': + specifier: ^2.2.1 + version: 2.2.1(vue@3.6.0-beta.9(typescript@5.9.3)) vue: specifier: beta version: 3.6.0-beta.9(typescript@5.9.3) @@ -497,6 +500,12 @@ packages: cpu: [x64] os: [win32] + '@phosphor-icons/vue@2.2.1': + resolution: {integrity: sha512-3RNg1utc2Z5RwPKWFkW3eXI/0BfQAwXgtFxPUPeSzi55jGYUq16b+UqcgbKLazWFlwg5R92OCLKjDiJjeiJcnA==} + engines: {node: '>=14'} + peerDependencies: + vue: '>=3.2.39' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2036,6 +2045,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.57.0': optional: true + '@phosphor-icons/vue@2.2.1(vue@3.6.0-beta.9(typescript@5.9.3))': + dependencies: + vue: 3.6.0-beta.9(typescript@5.9.3) + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/src/App.vue b/src/App.vue index 7e7b785..28b6dea 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,23 +1,32 @@ + + diff --git a/src/components/DotBeat.vue b/src/components/DotBeat.vue new file mode 100644 index 0000000..2090eab --- /dev/null +++ b/src/components/DotBeat.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/lib/__tests__/millidays.spec.ts b/src/lib/__tests__/millidays.spec.ts index a34d25c..39fa9e2 100644 --- a/src/lib/__tests__/millidays.spec.ts +++ b/src/lib/__tests__/millidays.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { beats, fromDate, now, toDate } from '@/lib/millidays'; +import { beats, timeToBeats, now, beatsToTime } from '@/lib/millidays'; describe('millidays.ts', () => { describe('beats function', () => { @@ -45,51 +45,51 @@ describe('millidays.ts', () => { }); }); - describe('fromDate function', () => { + describe('timeToBeats function', () => { it('returns formatted beat string from date', () => { const date = new Date('2026-03-22T12:00:00Z'); - const result = fromDate(date); + const result = timeToBeats(date); expect(result).toMatch(/^@\d+\.\d{2}$/); }); it('uses precision parameter', () => { const date = new Date('2026-03-22T12:00:00Z'); - const result0 = fromDate(date, 0); - const result3 = fromDate(date, 3); + const result0 = timeToBeats(date, 0); + const result3 = timeToBeats(date, 3); expect(result0).toMatch(/^@\d+$/); expect(result3).toMatch(/^@\d+\.\d{3}$/); }); it('pads with zeros to maintain format', () => { const date = new Date('2026-03-22T00:00:00Z'); - const result = fromDate(date); + const result = timeToBeats(date); const parts = result.split('@')[1] ?? []; expect(parts.length).toBeGreaterThanOrEqual(3); }); it('uses default precision of 2', () => { const date = new Date('2026-03-22T12:00:00Z'); - const result = fromDate(date); + const result = timeToBeats(date); const parts = result.split('.'); expect(parts[1]).toHaveLength(2); }); it('formats with @ prefix', () => { const date = new Date('2026-03-22T12:00:00Z'); - const result = fromDate(date); + const result = timeToBeats(date); expect(result).toMatch(/^@/); }); it('handles zero precision', () => { const date = new Date('2026-03-22T12:00:00Z'); - const result = fromDate(date, 0); + const result = timeToBeats(date, 0); expect(result).not.toContain('.'); }); it('returns consistent results for same time', () => { const date = new Date('2026-03-22T12:00:00Z'); - const result1 = fromDate(date, 2); - const result2 = fromDate(date, 2); + const result1 = timeToBeats(date, 2); + const result2 = timeToBeats(date, 2); expect(result1).toBe(result2); }); }); @@ -129,40 +129,40 @@ describe('millidays.ts', () => { }); }); - describe('toDate function', () => { + describe('beatsToTime function', () => { it('converts beats number to Date object', () => { - const result = toDate(500); + const result = beatsToTime(500); expect(result).toBeInstanceOf(Date); }); it('returns a valid Date', () => { - const result = toDate(500); + const result = beatsToTime(500); expect(!isNaN(result.getTime())).toBe(true); }); it('handles beats at start of day', () => { - const result = toDate(0); + const result = beatsToTime(0); expect(result).toBeInstanceOf(Date); }); it('handles beats at end of day', () => { - const result = toDate(999); + const result = beatsToTime(999); expect(result).toBeInstanceOf(Date); }); it('handles fractional beats', () => { - const result = toDate(500.5); + const result = beatsToTime(500.5); expect(result).toBeInstanceOf(Date); }); it('converts beats in ascending order to later dates', () => { - const date1 = toDate(100); - const date2 = toDate(500); + const date1 = beatsToTime(100); + const date2 = beatsToTime(500); expect(date2.getTime()).toBeGreaterThan(date1.getTime()); }); it('handles very small beats values', () => { - const result = toDate(0.001); + const result = beatsToTime(0.001); expect(result).toBeInstanceOf(Date); }); }); @@ -202,12 +202,12 @@ describe('millidays.ts', () => { expect(result).toBe(0); }); - it('fromDate returns same formatted beat for same UTC moment', () => { + it('timeToBeats returns same formatted beat for same UTC moment', () => { const date1 = new Date('2026-03-22T15:30:45.000+01:00'); const date2 = new Date('2026-03-22T16:30:45.000+02:00'); - const formatted1 = fromDate(date1, 2); - const formatted2 = fromDate(date2, 2); + const formatted1 = timeToBeats(date1, 2); + const formatted2 = timeToBeats(date2, 2); expect(formatted1).toBe(formatted2); }); diff --git a/src/lib/millidays.ts b/src/lib/millidays.ts index 2a46398..f3d56b3 100644 --- a/src/lib/millidays.ts +++ b/src/lib/millidays.ts @@ -19,33 +19,38 @@ export const beats = (date?: Date): number => { return beats >= 1000 ? beats - 1000 : beats; }; -export const fromDate = (date: Date, precision: number = 2): string => { +export const timeToBeats = (date: Date, precision: number = 2): string => { return format(beats(date), precision); }; -export const fromDateParts = (date: Date, precision: number = 2): string[] => { +export const timeToBeatsParts = (date: Date, precision: number = 2): string[] => { return format(beats(date), precision).substring(1).split('.'); }; export const now = (precision?: number): string => { - return fromDate(new Date(), precision ?? 2); + return timeToBeats(new Date(), precision ?? 2); }; export const nowParts = (precision?: number): string[] => { return now(precision).substring(1).split('.'); }; -export const toDate = (beats: number): Date => { - const seconds = (beats * 24 * 60 * 60) / 1000; - - const d = new Date(seconds * 1000); - - var newDate = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000); - - var offset = d.getTimezoneOffset() / 60; - var hours = d.getHours(); +export const beatsToTime = (beats: number): Date => { + const milliseconds = beats * 24 * 60 * 60; + return new Date(milliseconds); +}; - newDate.setHours(hours - offset - 2); +export const timeParts = (date?: Date) => { + const t = date ? date.toLocaleTimeString() : new Date().toLocaleTimeString(); + const [time, mode] = t.split(' '); + const parts = time?.split(':') ?? ['0', '0', '0']; + if (mode) { + parts.push(mode); + } + return parts; +}; - return newDate; +export const beatsToTimeParts = (beats?: number): string[] => { + const d = beatsToTime(beats ?? 0); + return timeParts(d); };