├── .eslintignore ├── .husky ├── pre-commit └── commit-msg ├── docs ├── types │ ├── stop.function.ts │ ├── DigitizedValue.type.ts │ ├── StartRuleNames.type.ts │ ├── DisposeFunction.type.ts │ ├── Stop.type.ts │ ├── clock.function.ts │ ├── defaultCharset.function.ts │ ├── timer.function.ts │ ├── useDigitizer.function.ts │ ├── Translator.type.ts │ ├── count.function.ts │ ├── counter.function.ts │ ├── sort.function.ts │ ├── Change.type.ts │ ├── GrammarSource.type.ts │ ├── eventEmitter.function.ts │ ├── CallbackParams.type.ts │ ├── getAnimationRate.function.ts │ ├── getTwelveHourFormat.function.ts │ ├── useDurationFormats.function.ts │ ├── AnyExpectation.interface.ts │ ├── ClassParts.interface.ts │ ├── ClassRange.type.ts │ ├── DateFlagFormatFunction.type.ts │ ├── DigitizedValues.type.ts │ ├── EndExpectation.interface.ts │ ├── alphanumeric.function.ts │ ├── elapsedTime.function.ts │ ├── fisherYatesShuffle.function.ts │ ├── range.function.ts │ ├── FlipClockThemeLabels.type.ts │ ├── pad.function.ts │ ├── characterRange.function.ts │ ├── parseDuration.function.ts │ ├── Callback.type.ts │ ├── DurationMapDefinition.type.ts │ ├── flipClock.function.ts │ ├── DurationFlagFormatter.type.ts │ ├── MatchArrayStructureOptions.type.ts │ ├── ParserTracer.interface.ts │ ├── castDigitizedGroup.function.ts │ ├── isDigitizedGroup.function.ts │ ├── trackChanges.function.ts │ ├── castDigitizedValues.function.ts │ ├── EventEmitterCallback.type.ts │ ├── UseCssDeclaration.type.ts │ ├── OtherExpectation.interface.ts │ ├── StopPredicateFunction.type.ts │ ├── castDigitizedString.function.ts │ ├── useCss.function.ts │ ├── CssDeclaration.type.ts │ ├── Event.type.ts │ ├── TrackChangesCallback.type.ts │ ├── UseDigitizer.type.ts │ ├── mergeCss.function.ts │ ├── stopWhen.function.ts │ ├── useDateFormats.function.ts │ ├── CSSProperties.interface.ts │ ├── UseDateFormats.type.ts │ ├── stopAfterChanges.function.ts │ ├── Theme.type.ts │ ├── Expectation.type.ts │ ├── getFilteredDuration.function.ts │ ├── LiteralExpectation.interface.ts │ ├── MergedCssDeclaration.type.ts │ ├── MatchArrayStructureCallback.type.ts │ ├── useCharset.function.ts │ ├── ClassExpectation.interface.ts │ ├── useDictionary.function.ts │ ├── useSequencer.function.ts │ ├── UseDurationFormats.type.ts │ ├── useDefinitionMap.function.ts │ ├── faceValue.function.ts │ ├── sub.function.ts │ ├── add.function.ts │ ├── theme.function.ts │ ├── FaceValueProps.type.ts │ ├── SourceText.interface.ts │ ├── FlipClockCssOptions.type.ts │ ├── Location.interface.ts │ ├── GrammarSourceObject.interface.ts │ ├── UseDictionary.type.ts │ ├── matchArrayStructure.function.ts │ ├── LocationRange.interface.ts │ ├── ClockProps.type.ts │ ├── UseDateFormatsOptions.type.ts │ ├── FlipClockThemeOptions.type.ts │ ├── ElapsedTimeProps.type.ts │ ├── ParserTracerEvent.type.ts │ ├── UseSequencer.type.ts │ ├── FlipClockProps.type.ts │ ├── Face.interface.ts │ ├── UseDefinitionMap.type.ts │ ├── UseCss.type.ts │ ├── SequencerOptions.type.ts │ ├── UseCharsetOptions.type.ts │ ├── ParseOptions.interface.ts │ ├── CounterProps.type.ts │ ├── AlphanumericProps.type.ts │ ├── SyntaxError.class.ts │ ├── Clock.class.ts │ ├── UseCharset.type.ts │ ├── EventEmitter.class.ts │ ├── FaceValue.class.ts │ ├── FaceHooks.interface.ts │ ├── ElapsedTime.class.ts │ ├── Counter.class.ts │ ├── Alphanumeric.class.ts │ ├── FlipClock.class.ts │ └── Timer.class.ts ├── .vitepress │ └── theme │ │ └── index.ts ├── reference │ ├── face.md │ ├── flipclock.md │ ├── clock.md │ ├── counter.md │ ├── elapsed-time.md │ ├── sequencer.md │ ├── face-value.md │ ├── event-hooks.md │ ├── alphanumeric.md │ ├── timer.md │ ├── event-emitter.md │ ├── definition.md │ ├── dictionary.md │ ├── date.md │ ├── digitizer.md │ ├── css.md │ ├── charset.md │ └── duration.md ├── guide │ ├── build-your-own-face.md │ ├── what-is-flipclock.md │ ├── what-is-a-face.md │ ├── getting-started.md │ ├── counter.md │ ├── core-concepts.md │ ├── basic-usage.md │ ├── alphanumeric.md │ ├── customizing-css.md │ ├── event-hooks.md │ ├── creating-a-theme.md │ ├── elapsed-time.md │ └── clock.md ├── components │ ├── Counter.vue │ ├── Clock.vue │ ├── ElapsedTime.vue │ ├── ClockTwentyFourHour.vue │ ├── ElapsedTimeTenSeconds.vue │ ├── CounterCountdown.vue │ ├── ElapsedTimeSinceEpoch.vue │ ├── ElapsedTimeUntilNextYear.vue │ ├── Alphanumeric.vue │ ├── AlphanumericBackwards.vue │ ├── AlphanumericShuffle.vue │ ├── CounterButtons.vue │ └── Intro.vue ├── why-flipclock.md └── index.md ├── src ├── themes │ └── index.ts ├── faces │ ├── index.ts │ └── Clock.ts ├── index.ts ├── helpers │ ├── index.ts │ ├── functions.ts │ ├── digitizer.ts │ ├── dictionary.ts │ ├── css.ts │ └── duration.ts ├── Face.ts ├── EventEmitter.ts ├── FaceValue.ts └── Timer.ts ├── .vscode ├── extensions.json └── launch.json ├── commitlint.config.cjs ├── dev ├── tsconfig.json ├── vite.config.ts ├── index.html └── index.tsx ├── .editorconfig ├── tsup.config.ts ├── env.d.ts ├── format.peg ├── .changeset ├── config.json └── README.md ├── .gitignore ├── CHANGELOG.md ├── test ├── helpers │ ├── functions.test.ts │ ├── definfition.test.ts │ ├── dictionary.test.ts │ ├── duration.test.ts │ ├── css.test.ts │ ├── digitizer.test.ts │ ├── date.test.ts │ └── charset.test.ts ├── EventEmitter.test.ts ├── FaceValue.test.ts ├── Timer.test.ts ├── faces │ ├── Clock.test.ts │ ├── ElapsedTime.test.ts │ └── Alphanumeric.test.ts └── index.tsx ├── .eslintrc ├── tsconfig.json ├── license.txt ├── README.md ├── vitest.config.ts ├── vite.config.ts ├── .github └── workflows │ ├── beta.yaml │ └── master.yaml ├── bin └── extractTypes.ts └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | !docs/.vitepress/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm exec lint-staged 2 | pnpm test -------------------------------------------------------------------------------- /docs/types/stop.function.ts: -------------------------------------------------------------------------------- 1 | function stop(): Stop; -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm exec -- commitlint --edit ${1} 2 | -------------------------------------------------------------------------------- /docs/types/DigitizedValue.type.ts: -------------------------------------------------------------------------------- 1 | type DigitizedValue = string; -------------------------------------------------------------------------------- /docs/types/StartRuleNames.type.ts: -------------------------------------------------------------------------------- 1 | type StartRuleNames = "array"; -------------------------------------------------------------------------------- /docs/types/DisposeFunction.type.ts: -------------------------------------------------------------------------------- 1 | type DisposeFunction = () => void; -------------------------------------------------------------------------------- /docs/types/Stop.type.ts: -------------------------------------------------------------------------------- 1 | type Stop = Readonly<{ 2 | stop: true; 3 | }>; -------------------------------------------------------------------------------- /docs/types/clock.function.ts: -------------------------------------------------------------------------------- 1 | function clock(props?: ClockProps): Clock; -------------------------------------------------------------------------------- /docs/types/defaultCharset.function.ts: -------------------------------------------------------------------------------- 1 | function defaultCharset(): string[]; -------------------------------------------------------------------------------- /docs/types/timer.function.ts: -------------------------------------------------------------------------------- 1 | function timer(interval?: number): Timer; -------------------------------------------------------------------------------- /docs/types/useDigitizer.function.ts: -------------------------------------------------------------------------------- 1 | function useDigitizer(): UseDigitizer; -------------------------------------------------------------------------------- /docs/types/Translator.type.ts: -------------------------------------------------------------------------------- 1 | type Translator = (value: K) => T; -------------------------------------------------------------------------------- /docs/types/count.function.ts: -------------------------------------------------------------------------------- 1 | function count(values: DigitizedValues): number; -------------------------------------------------------------------------------- /docs/types/counter.function.ts: -------------------------------------------------------------------------------- 1 | function counter(props?: CounterProps): Counter; -------------------------------------------------------------------------------- /docs/types/sort.function.ts: -------------------------------------------------------------------------------- 1 | function sort(map: Map): string[]; -------------------------------------------------------------------------------- /docs/types/Change.type.ts: -------------------------------------------------------------------------------- 1 | type Change = { 2 | from: R; 3 | to: R; 4 | }; -------------------------------------------------------------------------------- /docs/types/GrammarSource.type.ts: -------------------------------------------------------------------------------- 1 | type GrammarSource = string | GrammarSourceObject; -------------------------------------------------------------------------------- /docs/types/eventEmitter.function.ts: -------------------------------------------------------------------------------- 1 | function eventEmitter(): EventEmitter; -------------------------------------------------------------------------------- /docs/types/CallbackParams.type.ts: -------------------------------------------------------------------------------- 1 | type CallbackParams = T extends any[] ? T : never; -------------------------------------------------------------------------------- /docs/types/getAnimationRate.function.ts: -------------------------------------------------------------------------------- 1 | function getAnimationRate(el: Element): number; -------------------------------------------------------------------------------- /docs/types/getTwelveHourFormat.function.ts: -------------------------------------------------------------------------------- 1 | function getTwelveHourFormat(date: Date): string; -------------------------------------------------------------------------------- /docs/types/useDurationFormats.function.ts: -------------------------------------------------------------------------------- 1 | function useDurationFormats(): UseDurationFormats; -------------------------------------------------------------------------------- /docs/types/AnyExpectation.interface.ts: -------------------------------------------------------------------------------- 1 | interface AnyExpectation { 2 | readonly type: "any"; 3 | } -------------------------------------------------------------------------------- /docs/types/ClassParts.interface.ts: -------------------------------------------------------------------------------- 1 | interface ClassParts extends Array { 2 | } -------------------------------------------------------------------------------- /docs/types/ClassRange.type.ts: -------------------------------------------------------------------------------- 1 | type ClassRange = [ 2 | start: string, 3 | end: string, 4 | ] -------------------------------------------------------------------------------- /docs/types/DateFlagFormatFunction.type.ts: -------------------------------------------------------------------------------- 1 | type DateFlagFormatFunction = (date: Date) => string; -------------------------------------------------------------------------------- /docs/types/DigitizedValues.type.ts: -------------------------------------------------------------------------------- 1 | type DigitizedValues = (DigitizedValue | DigitizedValues)[]; -------------------------------------------------------------------------------- /docs/types/EndExpectation.interface.ts: -------------------------------------------------------------------------------- 1 | interface EndExpectation { 2 | readonly type: "end"; 3 | } -------------------------------------------------------------------------------- /docs/types/alphanumeric.function.ts: -------------------------------------------------------------------------------- 1 | function alphanumeric(props: AlphanumericProps): Alphanumeric; -------------------------------------------------------------------------------- /docs/types/elapsedTime.function.ts: -------------------------------------------------------------------------------- 1 | function elapsedTime(props?: ElapsedTimeProps): ElapsedTime; -------------------------------------------------------------------------------- /docs/types/fisherYatesShuffle.function.ts: -------------------------------------------------------------------------------- 1 | function fisherYatesShuffle(chars: string[]): string[]; -------------------------------------------------------------------------------- /docs/types/range.function.ts: -------------------------------------------------------------------------------- 1 | function range(startAt: number | undefined, size: number): number[]; -------------------------------------------------------------------------------- /docs/types/FlipClockThemeLabels.type.ts: -------------------------------------------------------------------------------- 1 | type FlipClockThemeLabels = (string | FlipClockThemeLabels)[]; -------------------------------------------------------------------------------- /docs/types/pad.function.ts: -------------------------------------------------------------------------------- 1 | function pad(value: string | number | undefined, length: number): string; -------------------------------------------------------------------------------- /docs/types/characterRange.function.ts: -------------------------------------------------------------------------------- 1 | function characterRange(startChar: string, endChar: string): string[]; -------------------------------------------------------------------------------- /docs/types/parseDuration.function.ts: -------------------------------------------------------------------------------- 1 | function parseDuration(duration: string | null | undefined): number; -------------------------------------------------------------------------------- /src/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './flipclock'; 2 | export * from './flipclock/flipclock.css'; 3 | 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/types/Callback.type.ts: -------------------------------------------------------------------------------- 1 | type Callback

, R = undefined> = (...args: P) => R; -------------------------------------------------------------------------------- /docs/types/DurationMapDefinition.type.ts: -------------------------------------------------------------------------------- 1 | type DurationMapDefinition = [keyof Duration, DurationFlagFormatter]; -------------------------------------------------------------------------------- /docs/types/flipClock.function.ts: -------------------------------------------------------------------------------- 1 | function flipClock>(props: FlipClockProps): FlipClock; -------------------------------------------------------------------------------- /docs/types/DurationFlagFormatter.type.ts: -------------------------------------------------------------------------------- 1 | type DurationFlagFormatter = (duration: Duration, length: number) => string; -------------------------------------------------------------------------------- /docs/types/MatchArrayStructureOptions.type.ts: -------------------------------------------------------------------------------- 1 | type MatchArrayStructureOptions = { 2 | backwards?: boolean; 3 | }; -------------------------------------------------------------------------------- /docs/types/ParserTracer.interface.ts: -------------------------------------------------------------------------------- 1 | interface ParserTracer { 2 | trace: (event: ParserTracerEvent) => void; 3 | } -------------------------------------------------------------------------------- /docs/types/castDigitizedGroup.function.ts: -------------------------------------------------------------------------------- 1 | function castDigitizedGroup(value?: DigitizedValue | DigitizedValues): DigitizedValues; -------------------------------------------------------------------------------- /docs/types/isDigitizedGroup.function.ts: -------------------------------------------------------------------------------- 1 | function isDigitizedGroup(value: DigitizedValues | DigitizedValue | undefined): boolean; -------------------------------------------------------------------------------- /docs/types/trackChanges.function.ts: -------------------------------------------------------------------------------- 1 | function trackChanges

(fn: TrackChangesCallback): Callback; -------------------------------------------------------------------------------- /docs/types/castDigitizedValues.function.ts: -------------------------------------------------------------------------------- 1 | function castDigitizedValues(value?: DigitizedValue | DigitizedValues): DigitizedValue[]; -------------------------------------------------------------------------------- /docs/types/EventEmitterCallback.type.ts: -------------------------------------------------------------------------------- 1 | type EventEmitterCallback> = (...args: Required[K][]) => void; -------------------------------------------------------------------------------- /docs/types/UseCssDeclaration.type.ts: -------------------------------------------------------------------------------- 1 | type UseCssDeclaration = (...args: T) => V; -------------------------------------------------------------------------------- /docs/types/OtherExpectation.interface.ts: -------------------------------------------------------------------------------- 1 | interface OtherExpectation { 2 | readonly type: "other"; 3 | readonly description: string; 4 | } -------------------------------------------------------------------------------- /docs/types/StopPredicateFunction.type.ts: -------------------------------------------------------------------------------- 1 | type StopPredicateFunction = any[]> = TrackChangesCallback; -------------------------------------------------------------------------------- /docs/types/castDigitizedString.function.ts: -------------------------------------------------------------------------------- 1 | function castDigitizedString(value?: DigitizedValue | DigitizedValues): DigitizedValue | undefined; -------------------------------------------------------------------------------- /docs/types/useCss.function.ts: -------------------------------------------------------------------------------- 1 | function useCss(fn: UseCssDeclaration): UseCss; -------------------------------------------------------------------------------- /docs/types/CssDeclaration.type.ts: -------------------------------------------------------------------------------- 1 | type CssDeclaration = { 2 | css: T; 3 | toString(): string; 4 | }; -------------------------------------------------------------------------------- /docs/types/Event.type.ts: -------------------------------------------------------------------------------- 1 | type Event = { 2 | key: keyof T; 3 | fn: EventEmitterCallback; 4 | unwatch: () => void; 5 | }; -------------------------------------------------------------------------------- /docs/types/TrackChangesCallback.type.ts: -------------------------------------------------------------------------------- 1 | type TrackChangesCallback

= (changes: Change[], ...args: P) => R | Stop; -------------------------------------------------------------------------------- /docs/types/UseDigitizer.type.ts: -------------------------------------------------------------------------------- 1 | type UseDigitizer = { 2 | digitize: (value: any) => DigitizedValues; 3 | isDigitized: (value: any) => boolean; 4 | }; -------------------------------------------------------------------------------- /docs/types/mergeCss.function.ts: -------------------------------------------------------------------------------- 1 | function mergeCss(source: TSource, target: TTarget): TSource; -------------------------------------------------------------------------------- /docs/types/stopWhen.function.ts: -------------------------------------------------------------------------------- 1 | function stopWhen

, R>(predicate: StopPredicateFunction

, fn: Callback): Callback; -------------------------------------------------------------------------------- /docs/types/useDateFormats.function.ts: -------------------------------------------------------------------------------- 1 | function useDateFormats(): UseDateFormats; 2 | function useDateFormats(options: UseDateFormatsOptions): UseDateFormats; -------------------------------------------------------------------------------- /src/faces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Alphanumeric'; 2 | export * from './Clock'; 3 | export * from './Counter'; 4 | export * from './ElapsedTime'; 5 | 6 | -------------------------------------------------------------------------------- /docs/types/CSSProperties.interface.ts: -------------------------------------------------------------------------------- 1 | interface CSSProperties extends Properties { 2 | [key: string]: CSSProperties | string | number | undefined | null; 3 | } -------------------------------------------------------------------------------- /docs/types/UseDateFormats.type.ts: -------------------------------------------------------------------------------- 1 | type UseDateFormats = UseDefinitionMap & { 2 | format: (date: Date, format: string) => string; 3 | }; -------------------------------------------------------------------------------- /docs/types/stopAfterChanges.function.ts: -------------------------------------------------------------------------------- 1 | function stopAfterChanges

, R>(totalChanges: number, fn: Callback): Callback; -------------------------------------------------------------------------------- /docs/types/Theme.type.ts: -------------------------------------------------------------------------------- 1 | type Theme> = { 2 | render: (el: Element, instance: FlipClock) => [Element, DisposeFunction]; 3 | } & FaceHooks; -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [0] 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /docs/types/Expectation.type.ts: -------------------------------------------------------------------------------- 1 | type Expectation = 2 | | AnyExpectation 3 | | ClassExpectation 4 | | EndExpectation 5 | | LiteralExpectation 6 | | OtherExpectation; -------------------------------------------------------------------------------- /docs/types/getFilteredDuration.function.ts: -------------------------------------------------------------------------------- 1 | function getFilteredDuration(start: Date | number, end: Date | number, keys: T[]): Pick; -------------------------------------------------------------------------------- /dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vite/client"] 5 | }, 6 | "exclude": ["node_modules", "dist"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/types/LiteralExpectation.interface.ts: -------------------------------------------------------------------------------- 1 | interface LiteralExpectation { 2 | readonly type: "literal"; 3 | readonly text: string; 4 | readonly ignoreCase: boolean; 5 | } -------------------------------------------------------------------------------- /docs/types/MergedCssDeclaration.type.ts: -------------------------------------------------------------------------------- 1 | type MergedCssDeclaration = { 2 | [K in keyof T | keyof U]: K extends keyof U ? U[K] : K extends keyof T ? T[K] : never; 3 | }; -------------------------------------------------------------------------------- /docs/types/MatchArrayStructureCallback.type.ts: -------------------------------------------------------------------------------- 1 | type MatchArrayStructureCallback = Callback<[value?: DigitizedValue, target?: DigitizedValue | DigitizedValues], DigitizedValue | undefined>; -------------------------------------------------------------------------------- /docs/types/useCharset.function.ts: -------------------------------------------------------------------------------- 1 | function useCharset(): UseCharset; 2 | function useCharset(options: UseCharsetOptions): UseCharset; 3 | function useCharset(options?: UseCharsetOptions): UseCharset; -------------------------------------------------------------------------------- /docs/types/ClassExpectation.interface.ts: -------------------------------------------------------------------------------- 1 | interface ClassExpectation { 2 | readonly type: "class"; 3 | readonly parts: ClassParts; 4 | readonly inverted: boolean; 5 | readonly ignoreCase: boolean; 6 | } -------------------------------------------------------------------------------- /docs/types/useDictionary.function.ts: -------------------------------------------------------------------------------- 1 | function useDictionary(terms: [string, T | Translator][]): UseDictionary; 2 | function useDictionary(terms: Record>): UseDictionary; -------------------------------------------------------------------------------- /docs/types/useSequencer.function.ts: -------------------------------------------------------------------------------- 1 | function useSequencer(): UseSequencer; 2 | function useSequencer(options: SequencerOptions): UseSequencer; 3 | function useSequencer(options?: SequencerOptions): UseSequencer; -------------------------------------------------------------------------------- /docs/types/UseDurationFormats.type.ts: -------------------------------------------------------------------------------- 1 | type UseDurationFormats = { 2 | /** 3 | * Format the start and end date into a string. 4 | */ 5 | format: (start: Date, end: Date, format: string) => string; 6 | }; -------------------------------------------------------------------------------- /docs/types/useDefinitionMap.function.ts: -------------------------------------------------------------------------------- 1 | function useDefinitionMap(items: T): UseDefinitionMap>; 2 | function useDefinitionMap(items: Record): UseDefinitionMap; -------------------------------------------------------------------------------- /docs/types/faceValue.function.ts: -------------------------------------------------------------------------------- 1 | function faceValue(value: T): FaceValue; 2 | function faceValue(value: T, props: FaceValueProps): FaceValue; 3 | function faceValue(value: T, props?: FaceValueProps): FaceValue; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /docs/types/sub.function.ts: -------------------------------------------------------------------------------- 1 | function sub< 2 | DateType extends Date, 3 | ResultDate extends Date = DateType, 4 | >( 5 | date: DateArg, 6 | duration: Duration, 7 | options?: SubOptions, 8 | ): ResultDate; -------------------------------------------------------------------------------- /docs/types/add.function.ts: -------------------------------------------------------------------------------- 1 | function add< 2 | DateType extends Date, 3 | ResultDate extends Date = DateType, 4 | >( 5 | date: DateArg, 6 | duration: Duration, 7 | options?: AddOptions | undefined, 8 | ): ResultDate; -------------------------------------------------------------------------------- /docs/types/theme.function.ts: -------------------------------------------------------------------------------- 1 | function theme(): Theme; 2 | function theme(options: FlipClockThemeOptions): Theme; 3 | function theme(options?: FlipClockThemeOptions): Theme; -------------------------------------------------------------------------------- /docs/types/FaceValueProps.type.ts: -------------------------------------------------------------------------------- 1 | type FaceValueProps = { 2 | /** 3 | * The digitized values. 4 | */ 5 | digits?: DigitizedValues; 6 | /** 7 | * The digitizer instance. 8 | */ 9 | digitizer?: UseDigitizer; 10 | }; -------------------------------------------------------------------------------- /docs/types/SourceText.interface.ts: -------------------------------------------------------------------------------- 1 | interface SourceText { 2 | /** 3 | * Identifier of an input that was used as a grammarSource in parse(). 4 | */ 5 | readonly source: GrammarSource; 6 | /** Source text of the input. */ 7 | readonly text: string; 8 | } -------------------------------------------------------------------------------- /docs/types/FlipClockCssOptions.type.ts: -------------------------------------------------------------------------------- 1 | type FlipClockCssOptions = { 2 | borderRadius?: string; 3 | fontSize?: string; 4 | fontFamily?: string; 5 | width?: string; 6 | height?: string; 7 | animationDuration?: string; 8 | animationDelay?: string; 9 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EventEmitter'; 2 | export * from './Face'; 3 | export * from './faces'; 4 | export * from './FaceValue'; 5 | export * from './FlipClock'; 6 | export * from './helpers'; 7 | export * from './themes'; 8 | export * from './Timer'; 9 | 10 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | dts: true, 6 | format: ['esm'], 7 | esbuildOptions(opts) { 8 | opts.entryNames = '[dir]/[name].dts'; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /docs/types/Location.interface.ts: -------------------------------------------------------------------------------- 1 | interface Location { 2 | /** Line in the parsed source (1-based). */ 3 | readonly line: number; 4 | /** Column in the parsed source (1-based). */ 5 | readonly column: number; 6 | /** Offset in the parsed source (0-based). */ 7 | readonly offset: number; 8 | } -------------------------------------------------------------------------------- /docs/types/GrammarSourceObject.interface.ts: -------------------------------------------------------------------------------- 1 | interface GrammarSourceObject { 2 | readonly toString: () => string; 3 | 4 | /** 5 | * If specified, allows the grammar source to be embedded in a larger file 6 | * at some offset. 7 | */ 8 | readonly offset?: undefined | ((loc: Location) => Location); 9 | } -------------------------------------------------------------------------------- /docs/types/UseDictionary.type.ts: -------------------------------------------------------------------------------- 1 | type UseDictionary = UseDefinitionMap> & { 2 | /** 3 | * Translate a key. If no key is found, use the fallback. 4 | */ 5 | translate(key: K): T; 6 | translate(key: K, fallback: T): T; 7 | translate(key: K, fallback?: T): T; 8 | }; -------------------------------------------------------------------------------- /docs/types/matchArrayStructure.function.ts: -------------------------------------------------------------------------------- 1 | function matchArrayStructure(current: DigitizedValues, target: DigitizedValues, fn?: MatchArrayStructureCallback): DigitizedValues; 2 | function matchArrayStructure(current: DigitizedValues, target: DigitizedValues, options: MatchArrayStructureOptions | undefined, fn?: MatchArrayStructureCallback): DigitizedValues; -------------------------------------------------------------------------------- /dev/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solid from 'vite-plugin-solid'; 3 | 4 | export default defineConfig({ 5 | optimizeDeps: { 6 | include: ['../src/helpers/parser.cjs'] 7 | }, 8 | plugins: [ 9 | solid(), 10 | ], 11 | build: { 12 | target: 'esnext', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './charset'; 2 | export * from './css'; 3 | export * from './date'; 4 | export * from './dictionary'; 5 | export * from './digitizer'; 6 | export * from './duration'; 7 | export * from './functions'; 8 | export * from './parser'; 9 | export * from './sequencer'; 10 | export * from './structure'; 11 | 12 | export { add, sub } from 'date-fns'; 13 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import { h } from 'vue'; 3 | import Intro from '../../components/Intro.vue'; 4 | import './styles.css'; 5 | 6 | export default { 7 | extends: DefaultTheme, 8 | Layout: () => { 9 | return h(DefaultTheme.Layout, null, { 10 | 'home-hero-image': () => h(Intro) 11 | }); 12 | } 13 | }; -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | env: { 4 | NODE_ENV: 'production' | 'development' 5 | PROD: boolean 6 | DEV: boolean 7 | } 8 | } 9 | namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: 'production' | 'development' 12 | PROD: boolean 13 | DEV: boolean 14 | } 15 | } 16 | } 17 | 18 | export { } 19 | 20 | -------------------------------------------------------------------------------- /format.peg: -------------------------------------------------------------------------------- 1 | array = value: (value: string { return () => value } / "[" value: array "]" { return value })* { 2 | for(const item of value) { 3 | if(typeof item === 'function') { 4 | value.splice(value.indexOf(item), 1, ...item()); 5 | } 6 | } 7 | 8 | return value; 9 | } 10 | 11 | string = value: char+ { return value.flat(1).filter(Boolean) } 12 | 13 | char = !([\[\]]). -------------------------------------------------------------------------------- /docs/reference/face.md: -------------------------------------------------------------------------------- 1 | # Face 2 | 3 | `Face` is an interface that is implemented by [Alphanumeric](./alphanumeric.md), [Clock](./clock.md), [ElapsedTime](./elapsed-time.md) and [Counter](./counter.md). The `Face` interface must be implemented if you are creating your own face. In addition to the required methods, all the [Event Hook](./event-hooks.md) methods are available too. 4 | 5 | <<< @/types/Face.interface.ts{ts} -------------------------------------------------------------------------------- /docs/types/LocationRange.interface.ts: -------------------------------------------------------------------------------- 1 | interface LocationRange { 2 | /** 3 | * A string or object that was supplied to the `parse()` call as the 4 | * `grammarSource` option. 5 | */ 6 | readonly source: GrammarSource; 7 | /** Position at the beginning of the expression. */ 8 | readonly start: Location; 9 | /** Position after the end of the expression. */ 10 | readonly end: Location; 11 | } -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "objectivehtml/FlipClock" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "public", 13 | "baseBranch": "master", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } -------------------------------------------------------------------------------- /docs/types/ClockProps.type.ts: -------------------------------------------------------------------------------- 1 | type ClockProps = { 2 | /** 3 | * Specify a date used to start the display on the clock. 4 | */ 5 | date?: Date; 6 | /** 7 | * A format string for how the date is displayed. 8 | */ 9 | format?: string; 10 | /** 11 | * A formatter to display the date in the given format. 12 | */ 13 | formatter?: UseDateFormats | UseDateFormatsOptions; 14 | }; -------------------------------------------------------------------------------- /docs/types/UseDateFormatsOptions.type.ts: -------------------------------------------------------------------------------- 1 | type UseDateFormatsOptions = { 2 | /** 3 | * The digitizer instance. 4 | */ 5 | digitizer?: UseDigitizer; 6 | /** 7 | * A translate function or Dictionary. 8 | */ 9 | translate?: Translator | UseDictionary; 10 | /** 11 | * The date format flags. 12 | */ 13 | formats?: Record; 14 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | docs/.vitepress/cache 4 | .wrangler 5 | .output 6 | .vercel 7 | .netlify 8 | .vinxi 9 | app.config.timestamp_*.js 10 | 11 | # Environment 12 | .env 13 | .env*.local 14 | 15 | # dependencies 16 | /node_modules 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | *.launch 23 | .settings/ 24 | 25 | # Temp 26 | gitignore 27 | 28 | # System Files 29 | .DS_Store 30 | Thumbs.db 31 | -------------------------------------------------------------------------------- /docs/reference/flipclock.md: -------------------------------------------------------------------------------- 1 | 2 | # FlipClock 3 | 4 | The `FlipClock` instance is the clock's controller. The core functions are `mount`, `unmount`, `start`, `stop`, or `toggle`. The clock also offers an event bus hat you can bind callbacks to the event hooks. 5 | 6 | ## Instantiate 7 | 8 | <<< @/types/flipClock.function.ts{TS} 9 | 10 | ## Props 11 | 12 | <<< @/types/FlipClockProps.type.ts{TS} 13 | 14 | ## Returns 15 | 16 | <<< @/types/FlipClock.class.ts -------------------------------------------------------------------------------- /docs/reference/clock.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Clock 6 | 7 | `Clock` displays a `Date` in the given format. 8 | 9 | 10 | 11 | <<< @/components/Clock.vue#imports,parent,example{ts} 12 | 13 | ## Instantiate 14 | 15 | <<< @/types/clock.function.ts{ts} 16 | 17 | ## Props 18 | 19 | <<< @/types/ClockProps.type.ts{TS} 20 | 21 | ## Returns 22 | 23 | <<< @/types/Clock.class.ts{TS} -------------------------------------------------------------------------------- /docs/types/FlipClockThemeOptions.type.ts: -------------------------------------------------------------------------------- 1 | type FlipClockThemeOptions = { 2 | /** 3 | * The CSS declarations used for the theme. 4 | */ 5 | css?: T | T[]; 6 | /** 7 | * The characters that should be rendered as dividers. 8 | */ 9 | dividers?: RegExp | string | string[]; 10 | /** 11 | * The labels that appear above the groups. 12 | */ 13 | labels?: FlipClockThemeLabels; 14 | }; -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid App 9 | 10 | 11 | 12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/reference/counter.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Counter 6 | 7 | `Counter` increments or decrements a numerical face one or more steps at a time. 8 | 9 | 10 | 11 | <<< @/components/Counter.vue#imports,parent,example{ts} 12 | 13 | ## Instantiate 14 | 15 | <<< @/types/counter.function.ts{ts} 16 | 17 | ## Props 18 | 19 | <<< @/types/CounterProps.type.ts{TS} 20 | 21 | ## Returns 22 | 23 | <<< @/types/Counter.class.ts{ts} -------------------------------------------------------------------------------- /docs/reference/elapsed-time.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Elapsed Time 6 | 7 | `ElapsedTime` displays the duration between two dates. 8 | 9 | 10 | 11 | <<< @/components/ElapsedTime.vue#imports,parent,example{ts} 12 | 13 | ## Instantiate 14 | 15 | <<< @/types/elapsedTime.function.ts{ts} 16 | 17 | ## Props 18 | 19 | <<< @/types/ElapsedTimeProps.type.ts{TS} 20 | 21 | ## Returns 22 | 23 | <<< @/types/ElapsedTime.class.ts{ts} -------------------------------------------------------------------------------- /docs/types/ElapsedTimeProps.type.ts: -------------------------------------------------------------------------------- 1 | type ElapsedTimeProps = { 2 | /** 3 | * The date from which the elapsed time is calculated. 4 | */ 5 | from?: Date; 6 | /** 7 | * The date to which the elapsed time is calculated. 8 | */ 9 | to?: Date; 10 | /** 11 | * A format string for how the duration is displayed. 12 | */ 13 | format?: string; 14 | /** 15 | * A formatter to display the duration in the given format. 16 | */ 17 | formatter?: UseDurationFormats; 18 | }; -------------------------------------------------------------------------------- /docs/reference/sequencer.md: -------------------------------------------------------------------------------- 1 | # Sequencer 2 | 3 | Use a [Charset](./charset.md) to increment or decrement changes to `DigitizedValues`. A sequencer has the ability to stop after a defined set of changes. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/useSequencer.function.ts{TS} 8 | 9 | ## Props 10 | 11 | <<< @/types/SequencerOptions.type.ts{TS} 12 | <<< @/types/StopPredicateFunction.type.ts{TS} 13 | 14 | ## Returns 15 | 16 | <<< @/types/UseSequencer.type.ts{TS} 17 | 18 | ## Returns 19 | 20 | <<< @/types/UseSequencer.type.ts{TS} -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /docs/types/ParserTracerEvent.type.ts: -------------------------------------------------------------------------------- 1 | type ParserTracerEvent 2 | = { 3 | readonly type: "rule.enter"; 4 | readonly rule: string; 5 | readonly location: LocationRange 6 | } 7 | | { 8 | readonly type: "rule.fail"; 9 | readonly rule: string; 10 | readonly location: LocationRange 11 | } 12 | | { 13 | readonly type: "rule.match"; 14 | readonly rule: string; 15 | readonly location: LocationRange 16 | /** Return value from the rule. */ 17 | readonly result: unknown; 18 | }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:5173/", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /docs/types/UseSequencer.type.ts: -------------------------------------------------------------------------------- 1 | type UseSequencer = { 2 | /** 3 | * The charset used by the sequencer. 4 | */ 5 | charset: string[]; 6 | /** 7 | * Decrement the current value towards the target value. 8 | */ 9 | decrement: (current: FaceValue, target: FaceValue, count?: number, backwards?: boolean) => FaceValue; 10 | /** 11 | * Increment the current value towards the target value. 12 | */ 13 | increment: (current: FaceValue, target: FaceValue, count?: number, backwards?: boolean) => FaceValue; 14 | }; -------------------------------------------------------------------------------- /docs/reference/face-value.md: -------------------------------------------------------------------------------- 1 | # FaceValue 2 | 3 | The `FaceValue` digitizes a value that can be used `Face` and then rendered by a `Theme`. A `FaceValue` is inherently reactive, so the clock will automatically re-render when the `FaceValue` changes. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/faceValue.function.ts 8 | 9 | ## Props 10 | 11 | <<< @/types/FaceValueProps.type.ts 12 | 13 | ## Returns 14 | 15 | <<< @/types/FaceValue.class.ts 16 | 17 | ## Usage 18 | 19 | ```ts 20 | const a = faceValue(1); 21 | 22 | a.value++; 23 | 24 | const b = value.copy(); 25 | 26 | a.compare(b) // true 27 | ``` -------------------------------------------------------------------------------- /docs/types/FlipClockProps.type.ts: -------------------------------------------------------------------------------- 1 | type FlipClockProps> = { 2 | /** 3 | * Automatically start the clock after it is mounted. 4 | */ 5 | autoStart?: boolean; 6 | /** 7 | * The face displayed on the clock. 8 | */ 9 | face: T; 10 | /** 11 | * The theme used to render the clock/ 12 | */ 13 | theme: Theme; 14 | /** 15 | * The timer that controls the clock interval. 16 | */ 17 | timer?: Timer | number; 18 | /** 19 | * The DOM element the clock is mounted. 20 | */ 21 | parent?: Element | null; 22 | }; -------------------------------------------------------------------------------- /docs/types/Face.interface.ts: -------------------------------------------------------------------------------- 1 | interface Face = any> extends FaceHooks { 2 | /** 3 | * The face's value to display. When this value changes, or a new 4 | * `FaceValue` instance has been returned, the clock will automatically 5 | * re-render. 6 | * 7 | * @public 8 | */ 9 | faceValue(): FaceValue; 10 | /** 11 | * This method is called with every timer interval. Use this to increment, 12 | * decrement or value change the `faceValue()`. 13 | * 14 | * @public 15 | */ 16 | interval(instance: FlipClock): void; 17 | } -------------------------------------------------------------------------------- /docs/types/UseDefinitionMap.type.ts: -------------------------------------------------------------------------------- 1 | type UseDefinitionMap = { 2 | /** 3 | * A map of key/value pairs. 4 | */ 5 | map: Map; 6 | /** 7 | * Define a new definition. 8 | */ 9 | define(key: string, value: T): void; 10 | define(key: [string, T][]): void; 11 | define(key: Record): void; 12 | define(key: string | [string, T][] | Record, value?: T): void; 13 | /** 14 | * Removes a definition. 15 | */ 16 | unset(keys: string): void; 17 | unset(keys: string[]): void; 18 | unset(keys: string | string[]): void; 19 | }; -------------------------------------------------------------------------------- /docs/reference/event-hooks.md: -------------------------------------------------------------------------------- 1 | # Event Hooks 2 | 3 | Hooks are fundemental to `FlipClock` api structure. These get called in the order they are defined. Hooks are triggered on `Face` and `Theme` instances, as well as the event bus. 4 | 5 | <<< @/types/FaceHooks.interface.ts 6 | 7 | ## Event Bus 8 | 9 | Using the `FlipClock` instance, the hooks are available using the event bus. 10 | 11 | ```ts 12 | import { flipClock } from 'flipclock'; 13 | 14 | const clock = flipClock({ 15 | // Your options here 16 | }); 17 | 18 | clock.on('afterCreate', (instance: FlipClock) => { 19 | console.log('created!') 20 | }); 21 | ``` -------------------------------------------------------------------------------- /docs/reference/alphanumeric.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Alphanumeric 6 | 7 | `Alphanumeric` displays abritrary values and flips to the next value in sequence (or randomly), similar to a mechanical flip board at a train station. 8 | 9 | 10 | 11 | <<< @/components/Alphanumeric.vue#imports,parent,example{ts} 12 | 13 | ## Instantiate 14 | 15 | <<< @/types/alphanumeric.function.ts{ts} 16 | 17 | ## Props 18 | 19 | <<< @/types/AlphanumericProps.type.ts{TS} 20 | 21 | ## Returns 22 | 23 | <<< @/types/Alphanumeric.class.ts{ts} 24 | -------------------------------------------------------------------------------- /docs/reference/timer.md: -------------------------------------------------------------------------------- 1 | # Timer 2 | 3 | `Timer` is based on `window.requestAnimationFrame` instead of `setInterval`. It can tick at any defined interval, and is fast, efficient, and non-blocking. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/timer.function.ts{ts} 8 | 9 | ## Returns 10 | 11 | <<< @/types/Timer.class.ts 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { timer } from 'flipclock'; 17 | 18 | const instance = timer(100); 19 | 20 | instance.start(() => { 21 | console.log('started') 22 | }); 23 | 24 | instance.stop(() => { 25 | console.log('stopped') 26 | }); 27 | 28 | instance.reset(() => { 29 | console.log('reset') 30 | }); 31 | ``` -------------------------------------------------------------------------------- /docs/types/UseCss.type.ts: -------------------------------------------------------------------------------- 1 | type UseCss = { 2 | /** 3 | * Get the CSS declaration. 4 | */ 5 | (...args: T): CssDeclaration; 6 | /** 7 | * Merge the given CSS into the current definition. 8 | */ 9 | merge: (fn: UseCssDeclaration) => UseCss>; 10 | /** 11 | * Extend the current definition with the given CSS. 12 | */ 13 | extend: (fn: UseCssDeclaration) => UseCss>; 14 | }; -------------------------------------------------------------------------------- /docs/reference/event-emitter.md: -------------------------------------------------------------------------------- 1 | # Event Emitter 2 | 3 | `EventEmitter` is extended by [FlipClock](./flipclock.md), which provides event bus on all clocks. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/eventEmitter.function.ts{ts} 8 | 9 | ## Returns 10 | 11 | <<< @/types/EventEmitter.class.ts{ts} 12 | 13 | ## Usage 14 | 15 | ```ts 16 | const emitter = eventEmitter<{ 17 | foo: () => void 18 | bar: () => void 19 | }>(); 20 | 21 | const foo = vi.fn(); 22 | const bar = vi.fn(); 23 | 24 | emitter.on('foo', foo); 25 | emitter.on('bar', bar); 26 | emitter.once('foo', foo); 27 | 28 | const unwatch = emitter.on('foo', foo); 29 | 30 | emitter.emit('foo'); 31 | 32 | unwatch(); 33 | ``` -------------------------------------------------------------------------------- /docs/types/SequencerOptions.type.ts: -------------------------------------------------------------------------------- 1 | type SequencerOptions = { 2 | /** 3 | * The charset instance or options used to generate a charset. 4 | */ 5 | charset?: UseCharset | UseCharsetOptions; 6 | /** 7 | * The options passed the function that ensure the array structures are the same. 8 | */ 9 | matchArray?: MatchArrayStructureOptions; 10 | /** 11 | * A call that determines when the sequencer should stop. 12 | */ 13 | stopWhen?: StopPredicateFunction<[current?: DigitizedValue, target?: DigitizedValue | DigitizedValues]>; 14 | /** 15 | * A number of changes that stops the sequencer. 16 | */ 17 | stopAfterChanges?: number; 18 | }; -------------------------------------------------------------------------------- /docs/types/UseCharsetOptions.type.ts: -------------------------------------------------------------------------------- 1 | type UseCharsetOptions = { 2 | /** 3 | * A function that returns an array of characters. 4 | */ 5 | charset?: () => string[]; 6 | /** 7 | * The empty character. Defaults to a space. 8 | */ 9 | emptyChar?: string; 10 | /** 11 | * Provide a shuffle function `boolean` to enable/disable the default shuffle. 12 | */ 13 | shuffle?: ((chars: string[]) => string[]) | boolean; 14 | /** 15 | * An array of characters to omit from the `charset`. 16 | */ 17 | blacklist?: string[]; 18 | /** 19 | * An array of characters to include in the `charset`. 20 | */ 21 | whitelist?: string[]; 22 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # flipclock 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - [`561580a`](https://github.com/objectivehtml/FlipClock/commit/561580a70baf2f6c89791f54f34792ef15e93acc) - Fixed package.json and README.md 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - Initial release 14 | 15 | ### Patch Changes 16 | 17 | - [`f03d6f2`](https://github.com/objectivehtml/FlipClock/commit/f03d6f2acebdf75142be03944143dacc4e896d35) - Updated README.md 18 | 19 | ## 1.0.0-beta.2 20 | 21 | ### Patch Changes 22 | 23 | - [`f03d6f2`](https://github.com/objectivehtml/FlipClock/commit/f03d6f2acebdf75142be03944143dacc4e896d35) - Updated README.md 24 | 25 | ## 1.0.0-beta.1 26 | 27 | ### Patch Changes 28 | 29 | Initial release 30 | -------------------------------------------------------------------------------- /docs/types/ParseOptions.interface.ts: -------------------------------------------------------------------------------- 1 | interface ParseOptions { 2 | /** 3 | * String or object that will be attached to the each `LocationRange` object 4 | * created by the parser. For example, this can be path to the parsed file 5 | * or even the File object. 6 | */ 7 | readonly grammarSource?: GrammarSource; 8 | readonly startRule?: T; 9 | readonly tracer?: ParserTracer; 10 | 11 | // Internal use only: 12 | readonly peg$library?: boolean; 13 | // Internal use only: 14 | peg$currPos?: number; 15 | // Internal use only: 16 | peg$silentFails?: number; 17 | // Internal use only: 18 | peg$maxFailExpected?: Expectation[]; 19 | // Extra application-specific properties 20 | [key: string]: unknown; 21 | } -------------------------------------------------------------------------------- /src/helpers/functions.ts: -------------------------------------------------------------------------------- 1 | export function parseDuration(duration: string | null | undefined): number { 2 | // const match = duration?.trim().split(',')[0]?.match(/^([+-]?(?:\d+\.?\d*|\.\d+))\s*(ms|s|m|h)$/i); 3 | const match = duration?.trim().split(',')[0]?.match(/^([+-]?)(\d+(?:\.\d+)?|\.\d+)\s*(ms|s|m|h)$/i); 4 | 5 | if (!match) { 6 | return 0; 7 | } 8 | 9 | const [,, val, unit] = match; 10 | 11 | return parseFloat(val!) * { 12 | ms: 1, 13 | s: 1000, 14 | m: 60000, 15 | h: 3600000 16 | }[unit!.toLowerCase()]!; 17 | } 18 | 19 | export function getAnimationRate(el: Element) { 20 | return parseDuration( 21 | getComputedStyle(el).getPropertyValue('--animation-duration') 22 | ); 23 | } -------------------------------------------------------------------------------- /docs/types/CounterProps.type.ts: -------------------------------------------------------------------------------- 1 | type CounterProps = { 2 | /** 3 | * Determines if a clock should count down instead of up. 4 | */ 5 | countdown?: boolean; 6 | /** 7 | * A format function for how the counter is displayed. 8 | */ 9 | format?: (value: number) => string; 10 | /** 11 | * A number formatter for how the counter is displayed. 12 | */ 13 | formatter?: Intl.NumberFormat; 14 | /** 15 | * The starting value of the counter. 16 | */ 17 | value?: FaceValue | number; 18 | /** 19 | * The number of steps the counter ticks each interval. 20 | */ 21 | step?: number; 22 | /** 23 | * The clock will automatically stop when the target value is reached. 24 | */ 25 | targetValue?: FaceValue | number; 26 | }; -------------------------------------------------------------------------------- /test/helpers/functions.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { getAnimationRate, parseDuration } from "../../src/helpers/functions"; 3 | 4 | it('converts a duration string to a number', () => { 5 | expect(parseDuration('')).toBe(0); 6 | expect(parseDuration('test')).toBe(0); 7 | expect(parseDuration('500')).toBe(0); 8 | expect(parseDuration('500ms')).toBe(500); 9 | expect(parseDuration('.5s')).toBe(500); 10 | expect(parseDuration('5s')).toBe(5000); 11 | expect(parseDuration('1m')).toBe(60000); 12 | expect(parseDuration('1h')).toBe(3600000); 13 | }); 14 | 15 | it('gets the animation rate', () => { 16 | const el = document.createElement('div'); 17 | 18 | el.style.setProperty('--animation-duration', '500ms'); 19 | 20 | expect(getAnimationRate(el)).toBe(500); 21 | }); -------------------------------------------------------------------------------- /docs/reference/definition.md: -------------------------------------------------------------------------------- 1 | # Definition 2 | 3 | Create a map of key/value pairs used to define definitions and their values. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/useDefinitionMap.function.ts{ts} 8 | 9 | ## Returns 10 | 11 | <<< @/types/UseDefinitionMap.type.ts{ts} 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { useDefinition } from 'flipclock'; 17 | 18 | const { define, unset } = useDefinition([ 19 | ['January', 'Enero'], 20 | ['February', 'Febrero'], 21 | ]); 22 | 23 | define('March', 'Marzo'); 24 | 25 | define({ 26 | April: 'Abril', 27 | May: 'Mayo' 28 | }); 29 | 30 | unset('January'); 31 | unset(['February', 'March']); 32 | ``` 33 | 34 | ::: tip 35 | These are just a few examples and far from complete. If you want to see a feature-complete example, check `tests/helpers/dictionary.test.ts` in the repo. 36 | ::: 37 | -------------------------------------------------------------------------------- /docs/guide/build-your-own-face.md: -------------------------------------------------------------------------------- 1 | # Build Your Own Face 2 | 3 | A core concept to `FlipClock` is that the internal implementations are done so with the publicly consumable API's. All the faces provided by library can be used an examples for how to build your own. 4 | 5 | ## Face Interface 6 | 7 | All faces must implement the `Face` interface. All the [Hooks](./event-hooks.md) are available to the `Face`. 8 | 9 | <<< @/types/Face.interface.ts{TS} 10 | 11 | ## Example 12 | 13 | Below is the actual source code to the `Counter` face. Here are couple of key notes: 14 | 15 | 1. The `Counter` enforces a numeric type `FaceValue`. 16 | 2. `increment` and `decrement` are public instance methods specific to this face. 17 | 3. `faceValue()` and `interval()` satisfy the requirements by the `Face` interface. 18 | 19 | <<< @/../src/faces/Counter.ts{TS} 20 | -------------------------------------------------------------------------------- /docs/components/Counter.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /docs/guide/what-is-flipclock.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Introduction 6 | 7 | FlipClock.js is an open source library written in Typescript designed for building clocks, counters, scoreboards, flipboards and more. 8 | 9 | The goal is provide well-tested, strongly typed, and flexible API's that are easy and straight forward to use. The codebase is well-documented and we strongly encourage community contribution and feedback. 10 | 11 | ## How It Works 12 | 13 | FlipClock.js is framework agnostic and can be used an any modern JavaScript environment, including React, Vue, Angular, Svelte, and Solid. Everything in the library is themeable, which gives you complete control over the markup, styling, and behavior of your clocks. 14 | 15 | ## Basic Example 16 | 17 | 18 | 19 | <<< @/components/Clock.vue#imports,parent,example{ts} -------------------------------------------------------------------------------- /docs/reference/dictionary.md: -------------------------------------------------------------------------------- 1 | # Dictionaries 2 | 3 | Create a [Definition](./definition.md) of terms used for translations. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/useDictionary.function.ts{TS} 8 | <<< @/types/Translator.type.ts{TS} 9 | 10 | ## Returns 11 | 12 | <<< @/types/UseDictionary.type.ts{TS} 13 | 14 | ## Usage 15 | 16 | ```ts 17 | import { useDictionary } from 'flipclock'; 18 | 19 | const { define, translate } = useDictionary({ 20 | foo: 'bar' 21 | }); 22 | 23 | console.log(translate('foo')); // 'bar' 24 | console.log(translate('bar')); // 'bar 25 | console.log(translate('bar', 'foo')); // 'foo' 26 | 27 | define('hello', 'hola'); 28 | 29 | console.log(translate('hello')); // 'hola' 30 | ``` 31 | 32 | ::: tip 33 | These are just a few examples and far from complete. If you want to see a feature-complete example, check `tests/helpers/dictionary.test.ts` in the repo. 34 | ::: 35 | -------------------------------------------------------------------------------- /docs/types/AlphanumericProps.type.ts: -------------------------------------------------------------------------------- 1 | type AlphanumericProps = { 2 | /** 3 | * The starting value of the clock. 4 | */ 5 | value: FaceValue | FaceValue; 6 | /** 7 | * The target value of the clock. 8 | */ 9 | targetValue?: FaceValue | FaceValue; 10 | /** 11 | * Determines if the clock increments or decrements towards the target. 12 | */ 13 | method?: 'increment' | 'decrement'; 14 | /** 15 | * Determines if the sequencer works forwards or backwards. 16 | */ 17 | direction?: 'auto' | 'forwards' | 'backwards'; 18 | /** 19 | * The sequencer instance or options used for the sequencer. 20 | */ 21 | sequencer?: UseSequencer | SequencerOptions; 22 | /** 23 | * Determines how many characters to skip with each interval. 24 | */ 25 | skipChars?: number; 26 | }; -------------------------------------------------------------------------------- /docs/reference/date.md: -------------------------------------------------------------------------------- 1 | # Dates 2 | 3 | Defines a [Dictionary](./dictionary.md) of date formatting functions. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/useDateFormats.function.ts{TS} 8 | 9 | ## Props 10 | 11 | <<< @/types/UseDateFormatsOptions.type.ts{ts} 12 | 13 | <<< @/types/useDateFormats.function.ts{TS} 14 | <<< @/types/UseDateFormatsOptions.type.ts{TS} 15 | <<< @/types/DateFlagFormatFunction.type.ts{TS} 16 | 17 | ## Returns 18 | 19 | <<< @/types/UseDateFormats.type.ts{TS} 20 | 21 | ## Usage 22 | 23 | ```ts 24 | const { format, parse } = useDateFormats(); 25 | 26 | format(new Date('2025-01-01'), 'MM/DD/YYYY')); // '01/01/2025' 27 | format(new Date('2025-01-01'), 'DDDD, MMMM YYYY')); // 'Monday, January 2025' 28 | ``` 29 | 30 | ::: tip 31 | These are just a few examples and far from complete. If you want to see a feature-complete example, check `tests/helpers/date.test.ts` in the repo. 32 | ::: -------------------------------------------------------------------------------- /docs/reference/digitizer.md: -------------------------------------------------------------------------------- 1 | # Digitizer 2 | 3 | Converts an abitrary value into individual digits. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/useDigitizer.function.ts{TS} 8 | 9 | ## Returns 10 | 11 | <<< @/types/UseDigitizer.type.ts{TS} 12 | <<< @/types/DigitizedValue.type.ts{TS} 13 | <<< @/types/DigitizedValues.type.ts{TS} 14 | 15 | ## Usage 16 | 17 | ```ts 18 | const { digitize, isDigitized } = useDigitizer(); 19 | 20 | console.log(digitize('hello')); // ['h', 'e', 'l', 'l', 'o'] 21 | console.log(digitize(['hello', 'world'])); // [['h', 'e', 'l', 'l', 'o'], ['w', 'o', 'r', 'l', 'd']] 22 | 23 | console.log(isDigitized(['hello'])); // false 24 | console.log(isDigitized(['h', 'e', 'l', 'l', 'o'])); // true 25 | ``` 26 | 27 | ::: tip 28 | These are just a few examples and far from complete. If you want to see a feature-complete example, check `tests/helpers/digitizer.test.ts` in the repo. 29 | ::: 30 | -------------------------------------------------------------------------------- /docs/guide/what-is-a-face.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Clock Faces 7 | 8 | The [Face](../reference/face.md) provides unique functionality to each clock. Consider a stop watch, a lunar clock, or counter – these all have different functions around time. The `Face` in FlipClock.js is no different. Each face has different options and methods, but each share a minimal unified API. 9 | 10 | ## Interface 11 | 12 | `faceValue()` and `interval()` are the only methods that are required on a face. Everything else is unique to each face. 13 | 14 | <<< @/types/Face.interface.ts 15 | 16 | ## Available Faces 17 | 18 | FlipClock.js provides 4 unique faces. 19 | 20 | 1. [Clock](./clock.md) 21 | 2. [Elapsed Time](./elapsed-time.md) 22 | 3. [Counter](./counter.md) 23 | 4. [Alphanumeric](./alphanumeric.md) -------------------------------------------------------------------------------- /docs/components/Clock.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /docs/components/ElapsedTime.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Getting Started 6 | 7 | ## Installation 8 | 9 | ### Package Manager 10 | 11 | Package manager like [pnpm](https://pnpm.io), [yarn](https://classic.yarnpkg.com/en/), [npm](https://www.npmjs.com), etc. 12 | 13 | ::: code-group 14 | ```bash [pnpm] 15 | pnpm i flipclock 16 | ``` 17 | 18 | ```bash [yarn] 19 | yarn add flipclock 20 | ``` 21 | 22 | ```bash [npm] 23 | npm i flipclock 24 | ``` 25 | 26 | ```bash [bun] 27 | bun i flipclock 28 | ``` 29 | ::: 30 | 31 | ### CDN 32 | 33 | ::: code-group 34 | ```html [JSDelivr] 35 | 36 | ``` 37 | 38 | ```html [Unpkg] 39 | 40 | ``` 41 | ::: 42 | 43 | ## Basic Example 44 | 45 | 46 | 47 | <<< @/components/Clock.vue#imports,parent,example{ts} -------------------------------------------------------------------------------- /docs/reference/css.md: -------------------------------------------------------------------------------- 1 | # CSS 2 | 3 | A powerful CSS-in-JS solution powered by [Goober](https://goober.js.org/) that can be merged and extended. 4 | 5 | ## Intantiate 6 | 7 | <<< @/types/useCss.function.ts{ts} 8 | 9 | ## Props 10 | 11 | <<< @/types/UseCssDeclaration.type.ts{ts} 12 | 13 | ## Returns 14 | 15 | <<< @/types/UseCss.type.ts{ts} 16 | <<< @/types/CssDeclaration.type.ts{ts} 17 | 18 | ## Usage 19 | 20 | ```ts 21 | import { useCss } from 'flipclock'; 22 | 23 | const css = useCss((background: string, color: string) => ({ 24 | body: { 25 | background: background, 26 | color: color, 27 | } 28 | })); 29 | 30 | const declaration = css('white', 'black'); // {body: {background: 'white', color: 'blacl'}} 31 | 32 | console.log(String(declaration)); // 'go3003028782' 33 | ``` 34 | 35 | ::: tip 36 | These are just a few examples and far from complete. If you want to see a feature-complete example, check `tests/helpers/css.test.ts` in the repo. 37 | ::: 38 | -------------------------------------------------------------------------------- /docs/components/ClockTwentyFourHour.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /docs/components/ElapsedTimeTenSeconds.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /docs/types/SyntaxError.class.ts: -------------------------------------------------------------------------------- 1 | class SyntaxError extends globalThis.SyntaxError { 2 | /** 3 | * Constructs the human-readable message from the machine representation. 4 | * 5 | * @param expected Array of expected items, generated by the parser 6 | * @param found Any text that will appear as found in the input instead of 7 | * expected 8 | */ 9 | static buildMessage(expected: Expectation[], found?: string | null | undefined): string; 10 | readonly expected: Expectation[]; 11 | readonly found: string | null | undefined; 12 | readonly location: LocationRange; 13 | readonly name: string; 14 | constructor( 15 | message: string, 16 | expected: Expectation[], 17 | found: string | null, 18 | location: LocationRange, 19 | ); 20 | 21 | /** 22 | * With good sources, generates a feature-rich error message pointing to the 23 | * error in the input. 24 | * @param sources List of {source, text} objects that map to the input. 25 | */ 26 | format(sources: SourceText[]): string; 27 | } -------------------------------------------------------------------------------- /docs/why-flipclock.md: -------------------------------------------------------------------------------- 1 | # Why FlipClock.js? 2 | 3 | FlipClock.js is a years long passion project that has been re-written many times that started on jQuery, long before TypeScript and adopted thousands of stars on Github. Over the years, the technical debt stacked up and other projects took priority. Next thing I know, years have past and a modern FlockClock.js is long overdue. 4 | 5 | It's the old adage, write once to figure out the problem, write twice to solve it. In the case of FlipClock, it took more than two times. I've attempted to rewrite the library several times over the years with several tech stacks. Each time coming back to the project with new ideas, more experience, and simpler ways to express concept ideas in code. 6 | 7 | FlipClock.js became my way to explore a singular narrow concept as far as I can, do it the best I possibly can, without any shortcuts, without any runtime dependencies, with great docs, and have fun doing it. I hope you enjoy using FlipClock.js as much as I did writing it. 8 | 9 | Sincerely, \ 10 | Justin Kimbrell -------------------------------------------------------------------------------- /docs/types/Clock.class.ts: -------------------------------------------------------------------------------- 1 | class Clock implements Face { 2 | /** 3 | * The starting date on the clock. If no date is set, the current time 4 | * will be used. 5 | * 6 | * @public 7 | */ 8 | readonly date: Date; 9 | /** 10 | * The current formatted value. 11 | * 12 | * @public 13 | */ 14 | readonly value: FaceValue; 15 | /** 16 | * The format string. 17 | * 18 | * @public 19 | */ 20 | format: string; 21 | /** 22 | * The duration formatter. 23 | * 24 | * @public 25 | */ 26 | formatter: UseDateFormats; 27 | /** 28 | * Instantiate the clock face. 29 | * 30 | * @public 31 | */ 32 | constructor(props?: ClockProps); 33 | /** 34 | * The face's current value. 35 | * 36 | * @public 37 | */ 38 | faceValue(): FaceValue; 39 | /** 40 | * Format the face value to the current date/time. 41 | * 42 | * @public 43 | */ 44 | interval(instance: FlipClock): void; 45 | } -------------------------------------------------------------------------------- /test/EventEmitter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, vi } from 'vitest'; 2 | import { eventEmitter } from '../src/EventEmitter'; 3 | 4 | it('binds and unbinds events to an emitter', () => { 5 | const emitter = eventEmitter<{ 6 | foo: () => void 7 | bar: () => void 8 | }>(); 9 | 10 | const foo = vi.fn(); 11 | const bar = vi.fn(); 12 | 13 | emitter.on('foo', foo); 14 | emitter.on('bar', bar); 15 | emitter.once('foo', foo); 16 | 17 | const unwatch = emitter.on('foo', foo); 18 | 19 | emitter.emit('foo'); 20 | 21 | expect(foo).toBeCalledTimes(2); 22 | 23 | unwatch(); 24 | 25 | emitter.emit('foo'); 26 | 27 | expect(foo).toBeCalledTimes(3); 28 | 29 | emitter.off('foo', foo); 30 | emitter.emit('foo'); 31 | 32 | expect(foo).toBeCalledTimes(3); 33 | 34 | emitter.on('bar', bar); 35 | emitter.emit('bar'); 36 | 37 | expect(bar).toBeCalledTimes(2); 38 | 39 | emitter.reset(); 40 | 41 | emitter.emit('bar'); 42 | 43 | expect(bar).toBeCalledTimes(2); 44 | }); -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "no-only-tests", "eslint-comments"], 5 | "ignorePatterns": ["node_modules", "dist"], 6 | "parserOptions": { 7 | "project": "./tsconfig.json", 8 | "tsconfigRootDir": ".", 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": "warn", 13 | "keyword-spacing": "warn", 14 | "semi": "warn", 15 | "prefer-const": "warn", 16 | "no-console": "warn", 17 | "no-debugger": "warn", 18 | "no-only-tests/no-only-tests": "warn", 19 | "@typescript-eslint/no-unused-vars": [ 20 | "warn", 21 | { 22 | "argsIgnorePattern": "^_", 23 | "varsIgnorePattern": "^_", 24 | "caughtErrorsIgnorePattern": "^_" 25 | } 26 | ], 27 | "@typescript-eslint/no-unnecessary-type-assertion": "warn", 28 | "@typescript-eslint/no-unnecessary-condition": "warn", 29 | "@typescript-eslint/no-useless-empty-export": "warn", 30 | "eslint-comments/no-unused-disable": "warn" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "moduleResolution": "bundler", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "noEmit": true, 12 | "isolatedModules": true, 13 | "skipLibCheck": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noUncheckedIndexedAccess": true, 17 | "jsx": "preserve", 18 | "jsxImportSource": "solid-js", 19 | "types": ["solid-js"], 20 | "baseUrl": ".", 21 | "paths": { 22 | "flipclock": ["./dist/index.d.ts"] 23 | }, 24 | "outDir": "./dist" 25 | }, 26 | "include": [ 27 | "src/index.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | "docs/**/*.vue", 31 | "docs/.vitepress/**/*.ts" 32 | ], 33 | "exclude": ["node_modules", "dist"], 34 | "typescript": { 35 | "preferences": { 36 | "relative": true 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/components/CounterCountdown.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /docs/components/ElapsedTimeSinceEpoch.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | -------------------------------------------------------------------------------- /docs/types/UseCharset.type.ts: -------------------------------------------------------------------------------- 1 | type UseCharset = { 2 | /** 3 | * The charset that was given. 4 | */ 5 | charset: string[]; 6 | /** 7 | * The empty character from the charset. 8 | */ 9 | emptyChar: string; 10 | /** 11 | * Determines if the given value is blacklisted. 12 | */ 13 | isBlacklisted: (value: DigitizedValue) => boolean; 14 | /** 15 | * Determines if the given value is whitelisted. 16 | */ 17 | isWhitelisted: (value: DigitizedValue) => boolean; 18 | /** 19 | * Get a chunk of the charset for the given count. 20 | */ 21 | chunk: (value: DigitizedValue | undefined, count: number) => string[]; 22 | /** 23 | * Gets the next characters in the charset for the given count. 24 | */ 25 | next: (value?: DigitizedValue, target?: DigitizedValue | DigitizedValues, count?: number) => DigitizedValue | undefined; 26 | /** 27 | * Gets the previous characters in the charset for the given count. 28 | */ 29 | prev: (value?: DigitizedValue, target?: DigitizedValue | DigitizedValues, count?: number) => DigitizedValue | undefined; 30 | }; -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Objective HTML, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/components/ElapsedTimeUntilNextYear.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | -------------------------------------------------------------------------------- /docs/guide/counter.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Counter 8 | 9 | [Counter](../reference/counter.md) increments are decrements a `number` in any given format. A counter can stop at a target or count indefinitely. 10 | 11 | ## Options 12 | 13 | <<< @/types/CounterProps.type.ts{ts} 14 | 15 | ## Basic Example 16 | 17 | The counter starts at `0` and counts indefinitely. 18 | 19 | 20 | 21 | <<< @/components/Counter.vue#imports,parent,example{ts} 22 | 23 | ## Countdown 24 | 25 | The counter starts at `10` and counts down to `0` where it stops. 26 | 27 | 28 | 29 | <<< @/components/CounterCountdown.vue#imports,parent,example{ts} 30 | 31 | ## Increment and Decrement 32 | 33 | The counter manually increments or decrements on button click. The counter will even show negative numbers. 34 | 35 | 36 | 37 | <<< @/components/CounterButtons.vue#imports,parent,example{ts} 38 | <<< @/components/CounterButtons.vue#buttons{html} -------------------------------------------------------------------------------- /docs/types/EventEmitter.class.ts: -------------------------------------------------------------------------------- 1 | class EventEmitter { 2 | /** 3 | * The registered events. 4 | * 5 | * @protected 6 | */ 7 | protected events: Event[]; 8 | /** 9 | * Emit an event. 10 | * 11 | * @public 12 | */ 13 | emit>(key: K, ...args: Required[K] extends (...args: infer P) => void ? P : any[]): void; 14 | /** 15 | * Listen for an event. This returns a function to unwatch the event. 16 | * 17 | * @public 18 | */ 19 | on>(key: K, fn: EventEmitterCallback): () => void; 20 | /** 21 | * Listen for an event once. 22 | * 23 | * @public 24 | */ 25 | once>(key: K, fn: EventEmitterCallback): () => void; 26 | /** 27 | * Stop listening for all events using a, or with a key and a function. 28 | * 29 | * @public 30 | */ 31 | off>(key: K): void; 32 | off>(key: K, fn: T[K]): void; 33 | /** 34 | * Reset the event bus and remove all watchers. 35 | * 36 | * @public 37 | */ 38 | reset(): void; 39 | } -------------------------------------------------------------------------------- /docs/reference/charset.md: -------------------------------------------------------------------------------- 1 | # Charset 2 | 3 | Charset, short for "character set", defines a set of characters. Charset can be sequential or random. 4 | 5 | ## Instantiate 6 | 7 | <<< @/types/useCharset.function.ts{ts} 8 | 9 | ## Props 10 | 11 | <<< @/types/UseCharsetOptions.type.ts{ts} 12 | 13 | ## Returns 14 | 15 | <<< @/types/UseCharset.type.ts{ts} 16 | 17 | ## Usage 18 | 19 | ```ts 20 | const { next, prev, chunk } = useCharset({ 21 | blacklist: ['#'], 22 | whitelist: ['@'] 23 | }); 24 | 25 | expect(next('a')).toBe('b'); 26 | expect(next('b')).toBe('c'); 27 | expect(next(' ')).toBeUndefined(); 28 | expect(next(undefined, 'a')).toBe('a'); 29 | expect(next(undefined, 'a', 2)).toBe('a'); 30 | 31 | expect(prev('b')).toBe('a'); 32 | expect(prev('c')).toBe('b'); 33 | expect(prev('a')).toBe(' '); 34 | expect(prev(' ')).toBeUndefined(); 35 | expect(prev('a', undefined, 2)).toBeUndefined(); 36 | 37 | expect(chunk(undefined, 1)).toStrictEqual(['a']); 38 | expect(chunk(undefined, 5)).toStrictEqual(['a', 'b', 'c', 'd', 'e']); 39 | ``` 40 | 41 | ::: tip 42 | These are just a few examples and far from complete. If you want to see a feature-complete example, check `tests/helpers/charset.test.ts` in the repo. 43 | ::: 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/objectivehtml/FlipClock/branch/master/graph/badge.svg)](https://codecov.io/gh/objectivehtml/FlipClock) 2 | [![Tests](https://github.com/objectivehtml/FlipClock/actions/workflows/master.yaml/badge.svg)](https://github.com/objectivehtml/FlipClock/actions/workflows/master.yaml/badge.svg) 3 | [![npm version](https://badge.fury.io/js/flipclock.svg)](https://badge.fury.io/js/flipclock) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | # FlipClock.js 7 | 8 | A full featured, themeable, type safe, and well tested library for clocks, timers, counters, and flipboards. 9 | 10 | ## Install 11 | 12 | ```bash 13 | pnpm add flipclock 14 | ``` 15 | 16 | ```bash 17 | npm i flipclock 18 | ``` 19 | 20 | ```bash 21 | yarn add flipclock 22 | ``` 23 | 24 | ## Documentation 25 | 26 | To check out docs, visit [https://flipclockjs.com/](https://flipclockjs.com/). 27 | 28 | ## Changelog 29 | 30 | Detailed changes for each release are documented in the [CHANGELOG](https://github.com/objectivehtml/FlipClock/blob/master/CHANGELOG.md). 31 | 32 | ## License 33 | 34 | FlipClock.js is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). -------------------------------------------------------------------------------- /docs/components/Alphanumeric.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /docs/components/AlphanumericBackwards.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /docs/guide/core-concepts.md: -------------------------------------------------------------------------------- 1 | # Core Concepts 2 | 3 | ## Clock 4 | 5 | Think of a clock in the real world. The "clock" is the housing for the buttons, has an internal timer, and may have one or many functions. Each clock has a unique interface and appearance. Using this metaphor, the `FlipClock` instance is the "clock". 6 | 7 | ## Face 8 | 9 | The clock `Face` determines the behavior and functionality. Some clocks display time in 12 hours, and others use a 24 hour format. A stop watch displays elapsed time. A train station flipboard has alphanumberic characters that show arrivals and destinations. Each face has its own unique functionality. 10 | 11 | ## FaceValue 12 | 13 | The `FaceValue` is responsible for digitizing the data which is used by the `Face`. Each face can implement the `FaceValue` uniquely. 14 | 15 | ## Theme 16 | 17 | The `Theme` determines how the `Face` is rendered. The `Theme` is more than just CSS. The `Theme` also renders the clock in the DOM. Each theme can have its own markup, animations, and CSS. 18 | 19 | ## CSS 20 | 21 | FlipClock.js offers a comprehensive CSS-in-JS solution, creating your own CSS for a new theme, or extending the existing CSS to override it however you with. Of course, nothing stops you from using traditional CSS either. 22 | -------------------------------------------------------------------------------- /docs/components/AlphanumericShuffle.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | -------------------------------------------------------------------------------- /dev/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'solid-js/web'; 2 | import { faceValue } from '../src/FaceValue'; 3 | import { flipClock } from '../src/FlipClock'; 4 | import { alphanumeric } from '../src/faces/Alphanumeric'; 5 | import { useCss } from '../src/helpers/css'; 6 | import { theme } from '../src/themes/flipclock'; 7 | import { css } from '../src/themes/flipclock/flipclock.css'; 8 | 9 | 10 | type CssProps = { 11 | background: string; 12 | color: string; 13 | } 14 | 15 | const a = useCss((props: CssProps) => ({ 16 | body: { 17 | background: props.background, 18 | color: props.color 19 | } 20 | })) 21 | 22 | a.merge((props) => ({ 23 | div: { 24 | color: props.color 25 | } 26 | })) 27 | 28 | const instance = flipClock({ 29 | parent: document.createElement('div'), 30 | autoStart: false, 31 | timer: 250, 32 | face: alphanumeric({ 33 | value: faceValue('[Hello][World!]'), 34 | targetValue: faceValue('[Nice][to][meet][you!]'), 35 | skipChars: 10, 36 | sequencer: { 37 | stopAfterChanges: 3 38 | } 39 | }), 40 | theme: theme({ 41 | css: css({ 42 | animationDuration: '150ms' 43 | }) 44 | }), 45 | }); 46 | 47 | render(() => <> 48 |
49 | 50 | , document.getElementById('app')!); 51 | 52 | instance.mount(document.getElementById('clock')!); -------------------------------------------------------------------------------- /docs/guide/basic-usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | The primary role of the [FlipClock](../reference/flipclock.md) instance is to `mount`, `unmount`, `start`, `stop` and `toggle` a clock. The [Face](../reference/face.md) is responsible for most of the functionality. 4 | 5 | ## Mounting and Unmounting 6 | 7 | Mounting binds the clock to the DOM, and unmounting removes it. Passing `parent` to the `FlipClockProps` will automatically bind it on creation. By not passing an element, you are required to mount it manually. 8 | 9 | ```ts 10 | import { flipClock } from 'flipclock'; 11 | 12 | const clock = flipClock({ 13 | // your options here... 14 | }); 15 | 16 | // Mount the clock 17 | clock.mount(document.querySelector('#clock')!); 18 | 19 | // Unmount the clock 20 | clock.unmount(); 21 | ``` 22 | 23 | ## Starting and stopping the clock 24 | 25 | The clock starts automatically by default. If `autoStart` is set to `false`, you must manually start the clock. 26 | 27 | ```ts 28 | import { flipClock, clock, theme, css } from 'flipclock'; 29 | 30 | const clock = flipClock({ 31 | // your options here... 32 | }); 33 | 34 | // Start the clock 35 | clock.start(() => { 36 | console.log('The clock started!') 37 | }); 38 | 39 | // Stop the clock 40 | clock.stop(() => { 41 | console.log('The clock stopped!') 42 | }); 43 | 44 | // Toggle starts the clock if stopped, and stops if started. 45 | clock.toggle(() => { 46 | console.log(`Status:`, clock.timer.isStopped) 47 | }); 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "FlipClock.js" 7 | # text: "" 8 | tagline: A full featured, themeable, type safe, and well tested library for clocks, timers, counters, and flipboards. 9 | actions: 10 | - theme: brand 11 | text: Getting Started 12 | link: /guide/getting-started 13 | - theme: alt 14 | text: Introduction 15 | link: /guide/what-is-flipclock 16 | 17 | features: 18 | - title: Themeable 19 | icon: 🎨 20 | details: Full control over rendering and markup, allowing you to customize the structure, styling, and behavior. 21 | - title: Robust Features 22 | icon: 🧰 23 | details: One tool with endless possibilities. Includes clocks, timers, counters and flipboards out of the box. 24 | - title: Extensible API 25 | icon: 🧩 26 | details: Easily extend, customize or override any part of the API. It’s built to adapt to your precise requirements. 27 | - title: Type Safe 28 | details: Written entirely in TypeScript, the codebase is strongly typed to ensure safety, clarity, a great IDE experience. 29 | icon: 🔒 30 | - title: Unit Tested 31 | icon: 🧪 32 | details: With full unit test coverage and meticulous testing, the code is highly stable and reliable. FlipClock.js is ready for production. 33 | - title: Framework Agnostic 34 | icon: 🧬 35 | details: FlipClock.js is ready to use within React, Vue, Angular, Svelte, Solid, or even vanilla JavaScript. Batteries are included. 36 | --- 37 | -------------------------------------------------------------------------------- /docs/types/FaceValue.class.ts: -------------------------------------------------------------------------------- 1 | class FaceValue> { 2 | /** 3 | * Parameters that are passed to the digiter. 4 | * 5 | * @public 6 | */ 7 | readonly digitizer: UseDigitizer; 8 | /** 9 | * The face's value. 10 | * 11 | * @protected 12 | */ 13 | protected $value: Signal; 14 | /** 15 | * The face's digits. 16 | * 17 | * @protected 18 | */ 19 | protected $digits: Signal; 20 | /** 21 | * Instantiate the face value. 22 | * 23 | * @public 24 | */ 25 | constructor(value: T, props?: FaceValueProps); 26 | /** 27 | * The digitized value. 28 | * 29 | * @public 30 | */ 31 | get digits(): DigitizedValues; 32 | /** 33 | * Set the digits from a `DigitizedValues`. 34 | * 35 | * @public 36 | */ 37 | set digits(value: DigitizedValues); 38 | /** 39 | * Get the length of the flattened digitized array. 40 | * 41 | * @public 42 | */ 43 | get length(): number; 44 | /** 45 | * Get the value. 46 | * 47 | * @public 48 | */ 49 | get value(): T; 50 | /** 51 | * Set the value. 52 | * 53 | * @public 54 | */ 55 | set value(value: Exclude); 56 | /** 57 | * Compare the face value with the given subject. 58 | * 59 | * @public 60 | */ 61 | compare(subject?: FaceValue): boolean; 62 | /** 63 | * Create a new instance with the given value. 64 | * 65 | * @public 66 | */ 67 | copy(): FaceValue; 68 | } -------------------------------------------------------------------------------- /docs/types/FaceHooks.interface.ts: -------------------------------------------------------------------------------- 1 | interface FaceHooks> { 2 | /** 3 | * The `afterCreate` hook. 4 | * 5 | * @public 6 | */ 7 | afterCreate?(instance: FlipClock): void; 8 | /** 9 | * The `beforeMount` hook. 10 | * 11 | * @public 12 | */ 13 | beforeMount?(instance: FlipClock): void; 14 | /** 15 | * The `afterMount` hook. 16 | * 17 | * @public 18 | */ 19 | afterMount?(instance: FlipClock): void; 20 | /** 21 | * The `beforeUnmount` hook. 22 | * 23 | * @public 24 | */ 25 | beforeUnmount?(instance: FlipClock): void; 26 | /** 27 | * The `afterUnmount` hook. 28 | * 29 | * @public 30 | */ 31 | afterUnmount?(instance: FlipClock): void; 32 | /** 33 | * The `beforeInterval` hook. 34 | * 35 | * @public 36 | */ 37 | beforeInterval?(instance: FlipClock): void; 38 | /** 39 | * The `afterInterval` hook. 40 | * 41 | * @public 42 | */ 43 | afterInterval?(instance: FlipClock): void; 44 | /** 45 | * The `beforeStart` hook. 46 | * 47 | * @public 48 | */ 49 | beforeStart?(instance: FlipClock): void; 50 | /** 51 | * The `afterStart` hook. 52 | * 53 | * @public 54 | */ 55 | afterStart?(instance: FlipClock): void; 56 | /** 57 | * The `beforeStop` hook. 58 | * 59 | * @public 60 | */ 61 | beforeStop?(instance: FlipClock): void; 62 | /** 63 | * The `afterStop` hook. 64 | * 65 | * @public 66 | */ 67 | afterStop?(instance: FlipClock): void; 68 | } -------------------------------------------------------------------------------- /docs/guide/alphanumeric.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Alphanumeric 8 | 9 | [Alphanumeric](../reference/alphanumeric.md) works like an analog flipboard at a train station. This face makes heavy use of the [Sequencer](../reference/sequencer.md) to increment and decrement the clock. 10 | 11 | ## Options 12 | 13 | <<< @/types/AlphanumericProps.type.ts{ts} 14 | 15 | ## Basic Example 16 | 17 | This basic example demonstrates a clock flipping from `Hellow World!` to `This is FlipClock.js`. The clock stops after `3` changes and skips `5` characters at a time until it reaches its destination. `stopAfterChanges` controls how many characters are being flipped at a single time. Adjusting the `skipChars` option will determine how fast the clock reaches its target value. 18 | 19 | 20 | 21 | <<< @/components/Alphanumeric.vue#imports,parent,example{ts} 22 | 23 | ## Flip Backwards 24 | 25 | The clock will automatically flip backwards when the target value length is less than the current value length. 26 | 27 | 28 | 29 | <<< @/components/AlphanumericBackwards.vue#imports,parent,example{ts} 30 | 31 | ## Shuffle the Charset 32 | 33 | Shuffling the charset randomly flips to characters until it reaches the target value. Many times shuffling the charset will result in the clock reaching the target faster because it doesn't have to flip through the entire sequence to reach its target. 34 | 35 | 36 | 37 | <<< @/components/AlphanumericShuffle.vue#imports,parent,example{ts} -------------------------------------------------------------------------------- /test/helpers/definfition.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { useDefinitionMap } from '../../src'; 3 | 4 | it('creates a definition map from entries', () => { 5 | const { define, unset, map } = useDefinitionMap([ 6 | ['foo', 'bar'], 7 | ['bar', 'foo'], 8 | ['one', 1] 9 | ]); 10 | 11 | expect(map.get('foo')).toBe('bar'); 12 | expect(map.get('bar')).toBe('foo'); 13 | 14 | unset('foo'); 15 | 16 | expect(map.get('foo')).toBeUndefined(); 17 | 18 | unset(['bar']); 19 | 20 | expect(map.get('bar')).toBeUndefined(); 21 | 22 | define('foo', 'bar'); 23 | 24 | define({ 25 | bar: 'foo', 26 | two: 2 27 | }); 28 | 29 | define([ 30 | ['three', 3] 31 | ]); 32 | 33 | expect(map.get('foo')).toBe('bar'); 34 | expect(map.get('bar')).toBe('foo'); 35 | expect(map.get('two')).toBe(2); 36 | expect(map.get('three')).toBe(3); 37 | }); 38 | 39 | it('creates a definition map from object', () => { 40 | const { define, unset, map } = useDefinitionMap({ 41 | foo: 'bar', 42 | bar: 'foo', 43 | one: 1 44 | }); 45 | 46 | expect(map.get('foo')).toBe('bar'); 47 | expect(map.get('bar')).toBe('foo'); 48 | 49 | unset('foo'); 50 | 51 | expect(map.get('foo')).toBeUndefined(); 52 | 53 | unset(['bar']); 54 | 55 | expect(map.get('bar')).toBeUndefined(); 56 | 57 | define('foo', 'bar'); 58 | 59 | define({ 60 | bar: 'foo', 61 | two: 2 62 | }); 63 | 64 | define([ 65 | ['three', 3] 66 | ]); 67 | 68 | expect(map.get('foo')).toBe('bar'); 69 | expect(map.get('bar')).toBe('foo'); 70 | expect(map.get('two')).toBe(2); 71 | expect(map.get('three')).toBe(3); 72 | }); -------------------------------------------------------------------------------- /docs/guide/customizing-css.md: -------------------------------------------------------------------------------- 1 | # Customizing CSS 2 | 3 | FlipClock.js allows you to easily override the [CSS](../reference/css.md) used by a theme. CSS definitions can even accept properties for dynamic declarations, giving you the ability to customize the CSS without the need to override it. 4 | 5 | ## How it works 6 | 7 | Let's say you want to adjust the animation duration and font size of the clock. 8 | 9 | ```ts 10 | import { css } from 'flipclock'; 11 | 12 | const declaration = css({ 13 | animationDuration: '100ms', 14 | fontSize: '3rem' 15 | }); 16 | ``` 17 | 18 | ### Extending the CSS 19 | 20 | You can even extend the CSS to create a new declaration based on another. 21 | 22 | ```ts 23 | import { css } from 'flipclock'; 24 | 25 | const declaration = css({ 26 | animationDuration: '100ms', 27 | fontSize: '3rem' 28 | }).extend((props) => ({ 29 | // your CSS overrides here. 30 | })); 31 | ``` 32 | 33 | ## Custom CSS 34 | 35 | This uses the default CSS for FlipClock.js as an example. Call `useCss` to declare the CSS. Then pass the declaration into the theme. 36 | 37 | ```ts 38 | import { clock, flipClock, theme, useCss } from 'flipclock'; 39 | 40 | 41 | 42 | const instance = flipClock({ 43 | face: clock(), 44 | theme: theme({ 45 | css 46 | }) 47 | }); 48 | ``` 49 | 50 | ## Using Tradition CSS 51 | 52 | Nothing prevents you from using traditional CSS. The CSS-in-JS is totally optional. To use CSS file, just import it like you normally would. This works with ` -------------------------------------------------------------------------------- /test/Timer.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 2 | import { timer } from '../src/Timer'; 3 | 4 | describe('Timer', () => { 5 | beforeEach(() => { 6 | vi.useFakeTimers(); 7 | }); 8 | 9 | afterEach(() => { 10 | vi.useRealTimers(); 11 | }); 12 | 13 | it('ticks at the given interval', () => { 14 | const startCallback = vi.fn(); 15 | const stopCallback = vi.fn(); 16 | const resetCallback = vi.fn(); 17 | 18 | const instance = timer(); 19 | 20 | expect(instance.elapsed).toBe(0); 21 | 22 | instance.lastLoop = 0; 23 | 24 | expect(instance.lastLoop).toBe(0); 25 | 26 | instance.start(startCallback); 27 | 28 | expect(instance.count).toBe(0); 29 | expect(instance.started).not.toBeUndefined(); 30 | expect(instance.lastLoop).not.toBe(0); 31 | 32 | vi.advanceTimersByTime(1000); 33 | 34 | expect(instance.elapsed).toBe(1000); 35 | expect(instance.elapsedSinceLastLoop).toBe(1000); 36 | 37 | vi.advanceTimersToNextFrame(); 38 | 39 | expect(instance.count).toBe(1); 40 | 41 | vi.advanceTimersByTime(1000); 42 | 43 | expect(instance.elapsed).toBeGreaterThan(2000); 44 | expect(instance.elapsedSinceLastLoop).toBe(1000); 45 | 46 | vi.advanceTimersToNextFrame(); 47 | 48 | expect(instance.count).toBe(2); 49 | 50 | vi.advanceTimersByTime(1000); 51 | 52 | expect(instance.elapsed).toBeGreaterThan(3000); 53 | expect(instance.elapsedSinceLastLoop).toBe(1000); 54 | 55 | vi.advanceTimersToNextFrame(); 56 | 57 | expect(instance.count).toBe(3); 58 | 59 | instance.reset(resetCallback); 60 | 61 | vi.advanceTimersByTime(1000); 62 | vi.advanceTimersToNextFrame(); 63 | 64 | expect(resetCallback).toBeCalledTimes(1); 65 | 66 | instance.stop(stopCallback); 67 | 68 | vi.advanceTimersByTime(1000); 69 | 70 | expect(stopCallback).toBeCalledTimes(1); 71 | 72 | // expect(instance.elapsed).toBeGreaterThan(3000); 73 | expect(instance.elapsedSinceLastLoop).toBe(0); 74 | 75 | vi.advanceTimersToNextFrame(); 76 | 77 | expect(instance.count).toBe(1); 78 | 79 | }); 80 | }); -------------------------------------------------------------------------------- /test/helpers/digitizer.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { useDigitizer } from '../../src/helpers/digitizer'; 3 | 4 | it('digitizes and undigitizes values', () => { 5 | const { digitize } = useDigitizer(); 6 | 7 | expect(digitize(undefined)).toEqual([]); 8 | expect(digitize(' ')).toEqual([' ']); 9 | expect(digitize(' ')).toEqual([' ', ' ']); 10 | expect(digitize('a ')).toEqual(['a', ' ']); 11 | expect(digitize('123')).toEqual(['1', '2', '3']); 12 | expect(digitize(['abc', ['1'], 'def'])).toEqual(['a', 'b', 'c', ['1'], 'd', 'e', 'f']); 13 | expect(digitize(['a', 'b', 'c', ['1'], 'd', 'e', 'f'])).toEqual(['a', 'b', 'c', ['1'], 'd', 'e', 'f']); 14 | expect(digitize(['1', '2', '3'])).toEqual(['1', '2', '3']); 15 | expect(digitize(['1', ['23']])).toEqual(['1', ['2', '3']]); 16 | expect(digitize(['1', 23])).toEqual(['1', ['2', '3']]); 17 | expect(digitize(['1', ['2', '3']])).toEqual(['1', ['2', '3']]); 18 | }); 19 | 20 | it('digitizes array structures', () => { 21 | const { digitize } = useDigitizer(); 22 | 23 | expect(digitize('[123]')).toStrictEqual([['1', '2', '3']]); 24 | expect(digitize('[1][2][3]')).toStrictEqual([['1'], ['2'], ['3']]); 25 | expect(digitize('[1[2[3]]]')).toStrictEqual([['1', ['2', ['3']]]]); 26 | expect(digitize(['[1[2[3]]]', '[4[5]]'])).toStrictEqual([['1', ['2', ['3']]], ['4', ['5']]]); 27 | expect(digitize(['[aa][bb]', '[cc][dd]'])).toStrictEqual([['a', 'a'], ['b', 'b'], ['c', 'c'], ['d', 'd']]); 28 | }); 29 | 30 | it('checks if a value is a digitized array', () => { 31 | const { isDigitized } = useDigitizer(); 32 | 33 | expect(isDigitized('1')).toBe(false); 34 | expect(isDigitized(1)).toBe(false); 35 | expect(isDigitized(['1', 1])).toBe(false); 36 | expect(isDigitized([' '])).toBe(true); 37 | expect(isDigitized(['1', ['2', ['3', []]]])).toBe(true); 38 | expect(isDigitized(['1', ['2', ['3', ['45']]]])).toBe(false); 39 | expect(isDigitized(['not', ['v', 'a', 'l', 'i', 'd']])).toBe(false); 40 | }); 41 | 42 | // it('comparing face values', () => { 43 | // expect(new FaceValue('hello world').compare(new FaceValue('hello world'))).toBe(true); 44 | // expect(new FaceValue('hello').compare(new FaceValue('world'))).toBe(false); 45 | // expect(new FaceValue(['a', ['b', ['c']]]).compare(new FaceValue(['a', ['b', ['c']]]))).toBe(true); 46 | // }); -------------------------------------------------------------------------------- /docs/types/FlipClock.class.ts: -------------------------------------------------------------------------------- 1 | class FlipClock> extends EventEmitter> { 2 | /** 3 | * Determines if the clock should automatically start when it is mounted. 4 | * 5 | * @public 6 | */ 7 | readonly autoStart: boolean; 8 | /** 9 | * The parent element to which the clock is mounted. 10 | * 11 | * @public 12 | */ 13 | parent?: Element; 14 | /** 15 | * The clock element. 16 | * 17 | * @public 18 | */ 19 | el?: Element; 20 | /** 21 | * The face used to display the clock. 22 | * 23 | * @public 24 | */ 25 | readonly face: T; 26 | /** 27 | * The face used to display the clock. 28 | * 29 | * @public 30 | */ 31 | readonly theme: Theme; 32 | /** 33 | * The face value displayed on the clock. 34 | * 35 | * @public 36 | */ 37 | readonly timer: Timer; 38 | /** 39 | * Dispose of the clock. 40 | * 41 | * @private 42 | */ 43 | protected dispose?: DisposeFunction; 44 | /** 45 | * Construct the FlipClock. 46 | * 47 | * @public 48 | */ 49 | constructor(props: FlipClockProps); 50 | /** 51 | * Get the animation rate of the clock. 52 | * 53 | * @public 54 | */ 55 | get animationRate(): number; 56 | /** 57 | * Mount the clock instance to the DOM. 58 | * 59 | * @public 60 | */ 61 | mount(parent: Element): this; 62 | /** 63 | * Start the clock instance. 64 | * 65 | * @public 66 | */ 67 | start(fn?: (instance: FlipClock) => void): this; 68 | /** 69 | * Stop the clock instance. 70 | * 71 | * @public 72 | */ 73 | stop(fn?: (instance: FlipClock) => void): this; 74 | /** 75 | * Toggle starting/stopping the clock instance. 76 | * 77 | * @public 78 | */ 79 | toggle(fn?: (instance: FlipClock) => void): this; 80 | /** 81 | * Unmount the clock instance from the DOM. 82 | * 83 | * @public 84 | */ 85 | unmount(): this; 86 | /** 87 | * Dispatch the event and call the method that corresponds to given hook. 88 | * 89 | * @protected 90 | */ 91 | protected hook>>(key: K, ...args: Required>[K] extends (...args: infer P) => void ? P : any[]): void; 92 | } -------------------------------------------------------------------------------- /docs/guide/elapsed-time.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Elapsed Time 9 | 10 | [Elapsed Time](../reference/elapsed-time.md) shows a [Duration](../reference/duration.md) between two `Date` objects. 11 | 12 | ## Options 13 | 14 | <<< @/types/ElapsedTimeProps.type.ts{ts} 15 | 16 | ## Available Formats 17 | 18 | For a full overview of [Durations](../reference/duration.md) refer to the reference. 19 | 20 | | Format | Description | 21 | | ------ | --------------------------------- | 22 | | `Y` | Outputs duration in years. | 23 | | `M` | Outputs duration in months. | 24 | | `W` | Outputs duration in weeks . | 25 | | `D` | Outputs duration in days. | 26 | | `h` | Outputs duration in hours. | 27 | | `m` | Outputs duration in minutes. | 28 | | `s` | Outputs duration in seconds. | 29 | 30 | ::: tip 31 | The number of formatting flags directly correlates to the minimum digits that will be shown. Consider an example where 1 year has passed, `Y` will result in `1`. `YY` will result in `01`, and `YYY` will result in `001`. This pattern can be used for all the flags. 32 | ::: 33 | 34 | ## Basic Example 35 | 36 | This example uses the default values. It calcuates the elapsed time from when the clock starts to the current time and is formatted with `[mm]:[ss]`. 37 | 38 | 39 | 40 | <<< @/components/ElapsedTime.vue#imports,parent,example{ts} 41 | 42 | ## Stops the Clock 43 | 44 | Countdown from now to 10 seconds in the future. The clock will stop it when it reaches 0. 45 | 46 | 47 | 48 | <<< @/components/ElapsedTimeTenSeconds.vue#imports,parent,example{ts} 49 | 50 | ## New Years Countdown 51 | 52 | Countdown to the next new year. Notice, this example also shows labels, which are formatted in the same structure as `[WW]:[DD]:[hh]:[mm]:[ss]`. 53 | 54 | 55 | 56 | <<< @/components/ElapsedTimeUntilNextYear.vue#imports,parent,example{ts} 57 | 58 | ## Time Since Unix Epoch 59 | 60 | Show the time since the Unix Epoch. 61 | 62 | 63 | 64 | <<< @/components/ElapsedTimeSinceEpoch.vue#imports,parent,example{ts} -------------------------------------------------------------------------------- /docs/types/Timer.class.ts: -------------------------------------------------------------------------------- 1 | class Timer { 2 | /** 3 | * The count increments with each interval. 4 | * 5 | * @protected 6 | */ 7 | protected $count: number; 8 | /** 9 | * The requestAnimationFrame handle number. 10 | * 11 | * @protected 12 | */ 13 | protected $handle?: number; 14 | /** 15 | * The number of milliseconds that define an interval. 16 | * 17 | * @public 18 | */ 19 | readonly interval: number; 20 | /** 21 | * The timestamp of the last loop. 22 | * 23 | * @protected 24 | */ 25 | protected $lastLoop?: number; 26 | /** 27 | * The date the timer starts. 28 | * 29 | * @protected 30 | */ 31 | protected $startDate?: Date; 32 | /** 33 | * Construct the timer. 34 | * 35 | * @public 36 | */ 37 | constructor(ms?: number); 38 | /** 39 | * Get the number of times the timer has ticked. 40 | * 41 | * @public 42 | */ 43 | get count(): number; 44 | /** 45 | * The `elapsed` attribute. 46 | * 47 | * @public 48 | */ 49 | get elapsed(): number; 50 | /** 51 | * The `elapsedSinceLastLoop` attribute. 52 | * 53 | * @public 54 | */ 55 | get elapsedSinceLastLoop(): number; 56 | /** 57 | * Determines if the Timer is currently running. 58 | * 59 | * @public 60 | */ 61 | get isRunning(): boolean; 62 | /** 63 | * Determines if the Timer is currently stopped. 64 | * 65 | * @public 66 | */ 67 | get isStopped(): boolean; 68 | /** 69 | * Get the last timestamp the timer looped. 70 | * 71 | * @public 72 | */ 73 | get lastLoop(): number; 74 | /** 75 | * Set the last timestamp the timer looped. 76 | * 77 | * @public 78 | */ 79 | set lastLoop(value: number); 80 | /** 81 | * Get the date object when the timer started. 82 | * 83 | * @public 84 | */ 85 | get started(): Date | undefined; 86 | /** 87 | * Resets the timer. If a callback is provided, re-start the clock. 88 | * 89 | * @public 90 | */ 91 | reset(fn?: (timer: Timer) => void): Timer; 92 | /** 93 | * Starts the timer. 94 | * 95 | * @public 96 | */ 97 | start(fn?: (timer: Timer) => void): Timer; 98 | /** 99 | * Stops the timer. 100 | * 101 | * @public 102 | */ 103 | stop(fn?: (timer: Timer) => void): Timer; 104 | } -------------------------------------------------------------------------------- /.github/workflows/beta.yaml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | # 3 | name: Prerelease 4 | 5 | on: 6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're 7 | # using the `master` branch as the default branch. 8 | push: 9 | branches: [beta] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: write 17 | pages: write 18 | id-token: write 19 | pull-requests: write 20 | packages: write 21 | 22 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 23 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 24 | concurrency: 25 | group: pages 26 | cancel-in-progress: false 27 | 28 | jobs: 29 | # Build job 30 | build: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | - uses: pnpm/action-setup@v3 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: 22 42 | cache: pnpm 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v4 45 | - name: Install dependencies 46 | run: pnpm install --frozen-lockfile 47 | - name: Run tests 48 | run: pnpm vitest run --coverage 49 | - name: Build 50 | run: pnpm build 51 | - name: Upload coverage reports to Codecov 52 | uses: codecov/codecov-action@v5 53 | with: 54 | token: ${{ secrets.CODECOV_TOKEN }} 55 | - name: Upload artifact 56 | uses: actions/upload-pages-artifact@v3 57 | with: 58 | path: docs/.vitepress/dist 59 | - name: Create Release Pull Request or Publish 60 | id: changesets 61 | uses: changesets/action@v1 62 | with: 63 | commit: "ci: version bump from changesets" 64 | publish: pnpm changeset publish 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 68 | 69 | # Deployment job 70 | deploy: 71 | environment: 72 | name: github-pages 73 | url: ${{ steps.deployment.outputs.page_url }} 74 | needs: build 75 | runs-on: ubuntu-latest 76 | name: Deploy 77 | steps: 78 | - name: Deploy to GitHub Pages 79 | id: deployment 80 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/master.yaml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | # 3 | name: Release 4 | 5 | on: 6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're 7 | # using the `master` branch as the default branch. 8 | push: 9 | branches: [master] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: write 17 | pages: write 18 | id-token: write 19 | pull-requests: write 20 | packages: write 21 | 22 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 23 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 24 | concurrency: 25 | group: pages 26 | cancel-in-progress: false 27 | 28 | jobs: 29 | # Build job 30 | build: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | - uses: pnpm/action-setup@v3 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: 22 42 | cache: pnpm 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v4 45 | - name: Install dependencies 46 | run: pnpm install --frozen-lockfile 47 | - name: Run tests 48 | run: pnpm vitest run --coverage 49 | - name: Build 50 | run: pnpm build 51 | - name: Upload coverage reports to Codecov 52 | uses: codecov/codecov-action@v5 53 | with: 54 | token: ${{ secrets.CODECOV_TOKEN }} 55 | - name: Upload artifact 56 | uses: actions/upload-pages-artifact@v3 57 | with: 58 | path: docs/.vitepress/dist 59 | - name: Create Release Pull Request or Publish 60 | id: changesets 61 | uses: changesets/action@v1 62 | with: 63 | commit: "ci: version bump from changesets" 64 | publish: pnpm changeset publish 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 68 | 69 | # Deployment job 70 | deploy: 71 | environment: 72 | name: github-pages 73 | url: ${{ steps.deployment.outputs.page_url }} 74 | needs: build 75 | runs-on: ubuntu-latest 76 | name: Deploy 77 | steps: 78 | - name: Deploy to GitHub Pages 79 | id: deployment 80 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /docs/components/Intro.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 75 | 76 | -------------------------------------------------------------------------------- /docs/guide/clock.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Clock 7 | 8 | [Clock](../reference/clock.md) shows a `Date` object in any given format. 9 | 10 | ## Options 11 | 12 | <<< @/types/ClockProps.type.ts{ts} 13 | 14 | ## Available Formats 15 | 16 | For a full overview of [Date Formatting](../reference/date.md) refer to the reference. 17 | 18 | | Format | Description | Outputs | 19 | | ------ | --------------------------- | --------- | 20 | | `Q` | The quarter year (1-4) | `1` | 21 | | `YYYY` | 4 digit year | `2024` | 22 | | `YY` | 2 digit year | `24` | 23 | | `M` | 1 digit month | `1` | 24 | | `MM` | 2 digit month | `01` | 25 | | `MMM` | Abbreviated month | `Jan` | 26 | | `MMMM` | The full month | `January` | 27 | | `D` | 1 digit day of the month | `1` | 28 | | `DD` | 2 digit day of the month | `01` | 29 | | `DDD` | Abbreviated day of the week | `Mon` | 30 | | `DDDD` | Full day of the week | `Monday` | 31 | | `H` | 1 digit hour (1-24) | `1` | 32 | | `HH` | 2 digit hour (01-24) | `01` | 33 | | `h` | 1 digit hour (1-12) | `1` | 34 | | `hh` | 2 digit hour (01-12) | `01` | 35 | | `m` | 1 digit minute | `1` | 36 | | `mm` | 2 digit minute | `01` | 37 | | `s` | 1 digit second | `1` | 38 | | `ss` | 2 digit second | `01` | 39 | | `v` | 1 digit millisecond | `1` | 40 | | `vv` | 2 digit millisecond | `01` | 41 | | `vvv` | 1 digit millisecond | `001` | 42 | | `vvvv` | 2 digit millisecond | `0001` | 43 | | `A` | "AM" or "PM" | `AM` | 44 | | `a` | "am" or "pm" | `am` | 45 | 46 | ## Basic Example 47 | 48 | This example uses the default values. It starts on the current time and is formatted with `[hh]:[mm]:[ss][A]`. 49 | 50 | 51 | 52 | <<< @/components/Clock.vue#imports,parent,example{ts} 53 | 54 | ## 24-Hour Clock 55 | 56 | Like real life, a clock is not always showing the current time. This example shows a 24-hour clock that starts on a specific date in the past. 57 | 58 | 59 | 60 | <<< @/components/ClockTwentyFourHour.vue#imports,parent,example{ts} 61 | 62 | ## Digitizing 63 | 64 | Digitizing in the process by which individual characters are display on a clock. Consider the following format: `[hh]:[mm]:[ss][A]`. The letters are date formatting flags. The brackets denote groups. Groups are not required, but allow for more control over the markup and styling of the clock. -------------------------------------------------------------------------------- /src/Face.ts: -------------------------------------------------------------------------------- 1 | import { type FaceValue } from './FaceValue'; 2 | import { type FlipClock } from './FlipClock'; 3 | 4 | /** 5 | * The hooks that are fired during the lifecycle. Hooks are triggered in the 6 | * order they are defined. Hooks may be implemented on a `Face`, `Theme`, or 7 | * as an event. 8 | * 9 | * @public 10 | */ 11 | export interface FaceHooks> { 12 | /** 13 | * The `afterCreate` hook. 14 | * 15 | * @public 16 | */ 17 | afterCreate?(instance: FlipClock): void; 18 | 19 | /** 20 | * The `beforeMount` hook. 21 | * 22 | * @public 23 | */ 24 | beforeMount?(instance: FlipClock): void; 25 | 26 | /** 27 | * The `afterMount` hook. 28 | * 29 | * @public 30 | */ 31 | afterMount?(instance: FlipClock): void; 32 | 33 | /** 34 | * The `beforeUnmount` hook. 35 | * 36 | * @public 37 | */ 38 | beforeUnmount?(instance: FlipClock): void; 39 | 40 | /** 41 | * The `afterUnmount` hook. 42 | * 43 | * @public 44 | */ 45 | afterUnmount?(instance: FlipClock): void; 46 | 47 | /** 48 | * The `beforeInterval` hook. 49 | * 50 | * @public 51 | */ 52 | beforeInterval?(instance: FlipClock): void; 53 | 54 | /** 55 | * The `afterInterval` hook. 56 | * 57 | * @public 58 | */ 59 | afterInterval?(instance: FlipClock): void; 60 | 61 | /** 62 | * The `beforeStart` hook. 63 | * 64 | * @public 65 | */ 66 | beforeStart?(instance: FlipClock): void; 67 | 68 | /** 69 | * The `afterStart` hook. 70 | * 71 | * @public 72 | */ 73 | afterStart?(instance: FlipClock): void; 74 | 75 | /** 76 | * The `beforeStop` hook. 77 | * 78 | * @public 79 | */ 80 | beforeStop?(instance: FlipClock): void; 81 | 82 | /** 83 | * The `afterStop` hook. 84 | * 85 | * @public 86 | */ 87 | afterStop?(instance: FlipClock): void; 88 | } 89 | 90 | /** 91 | * All faces must implement this interface. 92 | * 93 | * @public 94 | */ 95 | export declare interface Face = any> extends FaceHooks { 96 | 97 | /** 98 | * The face's value to display. When this value changes, or a new 99 | * `FaceValue` instance has been returned, the clock will automatically 100 | * re-render. 101 | * 102 | * @public 103 | */ 104 | faceValue(): FaceValue 105 | 106 | /** 107 | * This method is called with every timer interval. Use this to increment, 108 | * decrement or value change the `faceValue()`. 109 | * 110 | * @public 111 | */ 112 | interval(instance: FlipClock): void; 113 | 114 | } -------------------------------------------------------------------------------- /src/helpers/digitizer.ts: -------------------------------------------------------------------------------- 1 | import { parse } from './parser'; 2 | 3 | /** 4 | * A single digitized value. 5 | * 6 | * @public 7 | */ 8 | export type DigitizedValue = string; 9 | 10 | /** 11 | * An array of digitized values. 12 | * 13 | * @public 14 | */ 15 | export type DigitizedValues = (DigitizedValue | DigitizedValues)[]; 16 | 17 | /** 18 | * The default empty character for digitization. 19 | * 20 | * @public 21 | */ 22 | export const EMPTY_CHAR = ' '; 23 | 24 | /** 25 | * The return type for `useDigitizer()`. 26 | * 27 | * @public 28 | */ 29 | export type UseDigitizer = { 30 | digitize: (value: any) => DigitizedValues; 31 | isDigitized: (value: any) => boolean; 32 | } 33 | 34 | /** 35 | * Create a digiter that can be used to convert a string into arrays of 36 | * individual characters. 37 | * 38 | * @public 39 | */ 40 | export function useDigitizer(): UseDigitizer { 41 | /** 42 | * Parse a string, number or an array into `DigitizedValues`. 43 | * 44 | * @public 45 | */ 46 | function digitize(value?: number | string | DigitizedValue | DigitizedValues): DigitizedValues { 47 | if (value === undefined) { 48 | return []; 49 | } 50 | 51 | if (typeof value === 'string') { 52 | return value.match(/\[|\]/) ? parse(value) : Array.from(value); 53 | } 54 | 55 | if (typeof value === 'number') { 56 | return Array.from(value.toString()); 57 | } 58 | 59 | for (const item of value) { 60 | const index = value.indexOf(item); 61 | const response = digitize(item); 62 | 63 | if (typeof item == 'string') { 64 | value.splice(index, 1, ...response); 65 | } 66 | else { 67 | value.splice(index, 1, response); 68 | } 69 | } 70 | 71 | return value.filter(Boolean); 72 | } 73 | 74 | /** 75 | * Check if the value is the type `DigitizedValues`. 76 | * 77 | * @public 78 | */ 79 | function isDigitized(value: any): boolean { 80 | if (!Array.isArray(value)) { 81 | return false; 82 | } 83 | 84 | for (const i in value) { 85 | if (typeof value[i] === 'string' && value[i].length === 1) { 86 | continue; 87 | } 88 | 89 | if (!Array.isArray(value[i])) { 90 | return false; 91 | } 92 | else if (!value[i].length) { 93 | continue; 94 | } 95 | 96 | if (!isDigitized(value[i])) { 97 | return false; 98 | } 99 | } 100 | 101 | return true; 102 | } 103 | 104 | return { 105 | digitize, 106 | isDigitized 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The event instance. 3 | * 4 | * @public 5 | */ 6 | export type Event = { 7 | key: keyof T, 8 | fn: EventEmitterCallback, 9 | unwatch: () => void; 10 | } 11 | 12 | /** 13 | * The callback from the event emitter. 14 | * 15 | * @public 16 | */ 17 | export type EventEmitterCallback> = (...args: Required[K][]) => void 18 | 19 | /** 20 | * An event emitter to facilitate emitter and listening for events. 21 | * 22 | * @public 23 | */ 24 | export class EventEmitter { 25 | /** 26 | * The registered events. 27 | * 28 | * @protected 29 | */ 30 | protected events: Event[] = []; 31 | 32 | /** 33 | * Emit an event. 34 | * 35 | * @public 36 | */ 37 | public emit>(key: K, ...args: Required[K] extends (...args: infer P) => void ? P : any[]) { 38 | for (const event of this.events) { 39 | if (event.key !== key) { 40 | continue; 41 | } 42 | 43 | event.fn(...args); 44 | } 45 | } 46 | 47 | /** 48 | * Listen for an event. This returns a function to unwatch the event. 49 | * 50 | * @public 51 | */ 52 | public on>(key: K, fn: EventEmitterCallback): () => void { 53 | const unwatch = () => { 54 | const index = this.events.findIndex(event => { 55 | return event.key === key && event.fn === fn; 56 | }); 57 | 58 | this.events.splice(index, 1); 59 | }; 60 | 61 | this.events.push({ key, fn, unwatch }); 62 | 63 | return unwatch; 64 | } 65 | 66 | /** 67 | * Listen for an event once. 68 | * 69 | * @public 70 | */ 71 | once>(key: K, fn: EventEmitterCallback): () => void { 72 | const unwatch = this.on(key, (...args: T[K][]) => { 73 | fn(...args); 74 | unwatch(); 75 | }); 76 | 77 | return unwatch; 78 | } 79 | 80 | /** 81 | * Stop listening for all events using a, or with a key and a function. 82 | * 83 | * @public 84 | */ 85 | off>(key: K): void 86 | off>(key: K, fn: T[K]): void 87 | off>(key: K, fn?: T[K]): void { 88 | for (const event of this.events) { 89 | if (event.key === key && (!fn || fn === event.fn)) { 90 | event.unwatch(); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Reset the event bus and remove all watchers. 97 | * 98 | * @public 99 | */ 100 | reset(): void { 101 | this.events = []; 102 | } 103 | } 104 | 105 | export function eventEmitter() { 106 | return new EventEmitter(); 107 | } -------------------------------------------------------------------------------- /src/faces/Clock.ts: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'solid-js'; 2 | import { Face } from '../Face'; 3 | import { faceValue, type FaceValue } from '../FaceValue'; 4 | import { FlipClock } from '../FlipClock'; 5 | import { useDateFormats, type UseDateFormats, type UseDateFormatsOptions } from '../helpers/date'; 6 | 7 | /** 8 | * The `Clock` face options. 9 | * 10 | * @public 11 | */ 12 | export type ClockProps = { 13 | /** 14 | * Specify a date used to start the display on the clock. 15 | */ 16 | date?: Date; 17 | 18 | /** 19 | * A format string for how the date is displayed. 20 | */ 21 | format?: string; 22 | 23 | /** 24 | * A formatter to display the date in the given format. 25 | */ 26 | formatter?: UseDateFormats | UseDateFormatsOptions; 27 | } 28 | 29 | /** 30 | * This face will show a clock in a given format. * 31 | * 32 | * @public 33 | */ 34 | export class Clock implements Face { 35 | 36 | /** 37 | * The starting date on the clock. If no date is set, the current time 38 | * will be used. 39 | * 40 | * @public 41 | */ 42 | public readonly date: Date; 43 | 44 | /** 45 | * The current formatted value. 46 | * 47 | * @public 48 | */ 49 | public readonly value: FaceValue; 50 | 51 | /** 52 | * The format string. 53 | * 54 | * @public 55 | */ 56 | public format: string = '[hh]:[mm]:[ss][A]'; 57 | 58 | /** 59 | * The duration formatter. 60 | * 61 | * @public 62 | */ 63 | public formatter: UseDateFormats; 64 | 65 | /** 66 | * Instantiate the clock face. 67 | * 68 | * @public 69 | */ 70 | constructor(props?: ClockProps) { 71 | this.date = props?.date ?? new Date(); 72 | 73 | if (props?.format) { 74 | this.format = props.format; 75 | } 76 | 77 | if (props?.formatter === undefined) { 78 | this.formatter = useDateFormats(); 79 | } 80 | else if ('format' in props.formatter) { 81 | this.formatter = props.formatter; 82 | } 83 | else { 84 | this.formatter = useDateFormats(props.formatter); 85 | } 86 | 87 | this.value = createRoot(() => { 88 | return faceValue(this.formatter.format(this.date, this.format)); 89 | }); 90 | } 91 | 92 | /** 93 | * The face's current value. 94 | * 95 | * @public 96 | */ 97 | public faceValue(): FaceValue { 98 | return this.value; 99 | } 100 | 101 | /** 102 | * Format the face value to the current date/time. 103 | * 104 | * @public 105 | */ 106 | public interval(instance: FlipClock): void { 107 | this.value.value = this.formatter.format( 108 | new Date(this.date.getTime() + instance.timer.elapsed), this.format 109 | ); 110 | } 111 | } 112 | 113 | /** 114 | * Create a new `Clock` instance. 115 | * 116 | * @public 117 | */ 118 | export function clock(props?: ClockProps) { 119 | return new Clock(props); 120 | } 121 | -------------------------------------------------------------------------------- /test/faces/Clock.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { nextTick } from ".."; 3 | import { flipClock } from "../../src/FlipClock"; 4 | import { clock } from "../../src/faces"; 5 | import { useDateFormats } from "../../src/helpers/date"; 6 | import { useDictionary } from "../../src/helpers/dictionary"; 7 | import { theme } from "../../src/themes/flipclock"; 8 | 9 | describe('Clock', () => { 10 | beforeEach(() => { 11 | vi.useFakeTimers(); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.useRealTimers(); 16 | }); 17 | 18 | it('creates a clock without any options', () => { 19 | const instance = flipClock({ 20 | face: clock(), 21 | theme: theme() 22 | }); 23 | 24 | expect(instance.face.faceValue().value).toBeTypeOf('string'); 25 | }); 26 | 27 | it('creates a clock from the specified date', async () => { 28 | const date = new Date('2025-01-01 00:00:00'); 29 | 30 | const instance = flipClock({ 31 | parent: document.createElement('div'), 32 | face: clock({ 33 | date, 34 | format: 'mm:ss' 35 | }), 36 | theme: theme() 37 | }); 38 | 39 | expect(instance.face.faceValue().value).toBe('00:00'); 40 | 41 | nextTick(instance); 42 | 43 | expect(instance.face.faceValue().value).toBe('00:01'); 44 | 45 | nextTick(instance); 46 | 47 | expect(instance.face.faceValue().value).toBe('00:02'); 48 | 49 | nextTick(instance); 50 | 51 | expect(instance.face.faceValue().value).toBe('00:03'); 52 | }); 53 | 54 | it('creates a clock with a formatter', () => { 55 | const date = new Date; 56 | 57 | const instance = flipClock({ 58 | face: clock({ 59 | format: '[hh]:[mm]', 60 | formatter: useDateFormats() 61 | }), 62 | theme: theme() 63 | }); 64 | 65 | expect(instance.face.faceValue().value).toBe( 66 | instance.face.formatter.format(date, instance.face.format) 67 | ); 68 | }); 69 | 70 | it('creates a clock with formatter options', () => { 71 | const date = new Date('2025-01-01 00:00:00'); 72 | 73 | const instance = flipClock({ 74 | face: clock({ 75 | date, 76 | format: 'MMMM', 77 | formatter: { 78 | translate: useDictionary({ 79 | 'January': 'Enero', 80 | 'February': 'Febrero', 81 | 'March': 'Marzo', 82 | 'April': 'Abril', 83 | 'May': 'Mayo', 84 | 'June': 'Junio', 85 | 'July': 'Julio', 86 | 'August': 'Agosto', 87 | 'September': 'Septiembre', 88 | 'October': 'Octubre', 89 | 'November': 'Noviembre', 90 | 'December': 'Diciembre' 91 | }) 92 | } 93 | }), 94 | theme: theme() 95 | }); 96 | 97 | expect(instance.face.faceValue().value).toBe('Enero'); 98 | }); 99 | }); -------------------------------------------------------------------------------- /src/FaceValue.ts: -------------------------------------------------------------------------------- 1 | import { createSignal, type Signal } from 'solid-js'; 2 | import { DigitizedValues, UseDigitizer, useDigitizer } from './helpers/digitizer'; 3 | import { count } from './helpers/structure'; 4 | 5 | /** 6 | * The `FaceValue` face options. 7 | * 8 | * @public 9 | */ 10 | export type FaceValueProps = { 11 | /** 12 | * The digitized values. 13 | */ 14 | digits?: DigitizedValues; 15 | 16 | /** 17 | * The digitizer instance. 18 | */ 19 | digitizer?: UseDigitizer; 20 | } 21 | 22 | /** 23 | * The FaceValue class digitizes the raw value and so it can be used by the 24 | * clock face. 25 | * 26 | * @public 27 | */ 28 | export class FaceValue> { 29 | 30 | /** 31 | * Parameters that are passed to the digiter. 32 | * 33 | * @public 34 | */ 35 | public readonly digitizer: UseDigitizer; 36 | 37 | /** 38 | * The face's value. 39 | * 40 | * @protected 41 | */ 42 | protected $value: Signal; 43 | 44 | /** 45 | * The face's digits. 46 | * 47 | * @protected 48 | */ 49 | protected $digits: Signal; 50 | 51 | /** 52 | * Instantiate the face value. 53 | * 54 | * @public 55 | */ 56 | constructor(value: T, props?: FaceValueProps) { 57 | this.digitizer = props?.digitizer || useDigitizer(); 58 | this.$value = createSignal(value); 59 | this.$digits = createSignal(props?.digits ?? this.digitizer.digitize(value)); 60 | } 61 | 62 | /** 63 | * The digitized value. 64 | * 65 | * @public 66 | */ 67 | public get digits() { 68 | return this.$digits[0](); 69 | } 70 | 71 | /** 72 | * Set the digits from a `DigitizedValues`. 73 | * 74 | * @public 75 | */ 76 | public set digits(value: DigitizedValues) { 77 | this.$digits[1](value); 78 | } 79 | 80 | /** 81 | * Get the length of the flattened digitized array. 82 | * 83 | * @public 84 | */ 85 | public get length() { 86 | return count(this.$digits[0]()); 87 | } 88 | 89 | /** 90 | * Get the value. 91 | * 92 | * @public 93 | */ 94 | public get value(): T { 95 | return this.$value[0](); 96 | } 97 | 98 | /** 99 | * Set the value. 100 | * 101 | * @public 102 | */ 103 | public set value(value: Exclude) { 104 | this.$value[1](value); 105 | this.$digits[1](this.digitizer.digitize(value)); 106 | } 107 | 108 | /** 109 | * Compare the face value with the given subject. 110 | * 111 | * @public 112 | */ 113 | public compare(subject?: FaceValue) { 114 | return JSON.stringify(this.digits) === JSON.stringify(subject?.digits); 115 | } 116 | 117 | /** 118 | * Create a new instance with the given value. 119 | * 120 | * @public 121 | */ 122 | public copy(): FaceValue { 123 | return new FaceValue(this.value, { 124 | digits: this.digits, 125 | digitizer: this.digitizer 126 | }); 127 | } 128 | } 129 | 130 | /** 131 | * Create a new `FaceValue` instance. 132 | * 133 | * @public 134 | */ 135 | export function faceValue(value: T): FaceValue; 136 | export function faceValue(value: T, props: FaceValueProps): FaceValue; 137 | export function faceValue(value: T, props?: FaceValueProps): FaceValue; 138 | export function faceValue(value: T, props?: FaceValueProps): FaceValue { 139 | return new FaceValue(value, props); 140 | } -------------------------------------------------------------------------------- /src/helpers/dictionary.ts: -------------------------------------------------------------------------------- 1 | export type UseDefinitionMap = { 2 | /** 3 | * A map of key/value pairs. 4 | */ 5 | map: Map; 6 | 7 | /** 8 | * Define a new definition. 9 | */ 10 | define(key: string, value: T): void; 11 | define(key: [string, T][]): void; 12 | define(key: Record): void; 13 | define(key: string | [string, T][] | Record, value?: T): void; 14 | 15 | /** 16 | * Removes a definition. 17 | */ 18 | unset(keys: string): void; 19 | unset(keys: string[]): void; 20 | unset(keys: string | string[]): void; 21 | } 22 | 23 | type InferType = T extends [string, infer V][] ? V : never; 24 | 25 | export function useDefinitionMap(items: T): UseDefinitionMap>; 26 | export function useDefinitionMap(items: Record): UseDefinitionMap; 27 | export function useDefinitionMap(items: [string, T][] | Record): UseDefinitionMap { 28 | const map = new Map(Array.isArray(items) ? items : Object.entries(items)); 29 | 30 | function define(key: string, value: T): void; 31 | function define(key: [string, T][]): void; 32 | function define(key: Record): void; 33 | function define(key: string | [string, T][] | Record, value?: T): void { 34 | if (typeof key === 'string' && value !== undefined) { 35 | map.set(key, value); 36 | } 37 | else if (Array.isArray(key)) { 38 | for (const [entryKey, entryValue] of key) { 39 | map.set(entryKey, entryValue); 40 | } 41 | } 42 | else if (typeof key === 'object') { 43 | for (const [entryKey, entryValue] of Object.entries(key)) { 44 | map.set(entryKey, entryValue); 45 | } 46 | } 47 | } 48 | 49 | function unset(keys: string | string[]): void { 50 | if (Array.isArray(keys)) { 51 | for (const key of keys) { 52 | map.delete(key); 53 | } 54 | } 55 | else { 56 | map.delete(keys); 57 | } 58 | } 59 | 60 | return { 61 | map, define, unset 62 | }; 63 | } 64 | 65 | /** 66 | * The translator function. 67 | */ 68 | export type Translator = (value: K) => T; 69 | 70 | /** 71 | * The return type for `useDictionary()`. 72 | * 73 | * @public 74 | */ 75 | export type UseDictionary = UseDefinitionMap> & { 76 | /** 77 | * Translate a key. If no key is found, use the fallback. 78 | */ 79 | translate(key: K): T; 80 | translate(key: K, fallback: T): T; 81 | translate(key: K, fallback?: T): T; 82 | } 83 | 84 | /** 85 | * Use the provided terms to create a reusable translation dictionary. 86 | * 87 | * @public 88 | */ 89 | export function useDictionary(terms: [string, T | Translator][]): UseDictionary; 90 | export function useDictionary(terms: Record>): UseDictionary; 91 | export function useDictionary(terms: [string, T | Translator][] | Record>): UseDictionary { 92 | const { map, define, unset } = useDefinitionMap( 93 | Array.isArray(terms) ? terms : Object.entries(terms) 94 | ); 95 | 96 | function translate(key: string): T; 97 | function translate(key: string, fallback: T): T; 98 | function translate(key: string, fallback?: T): T { 99 | const term = map.get(key); 100 | 101 | if (typeof term === 'function') { 102 | return (term as Translator)(key); 103 | } 104 | 105 | if (term !== undefined) { 106 | return term as T; 107 | } 108 | 109 | if (fallback !== undefined) { 110 | return fallback; 111 | } 112 | 113 | return key as unknown as T; 114 | } 115 | 116 | return { 117 | map, 118 | define, 119 | translate, 120 | unset, 121 | }; 122 | } -------------------------------------------------------------------------------- /src/helpers/css.ts: -------------------------------------------------------------------------------- 1 | import type { Properties } from "csstype"; 2 | import { css } from "goober"; 3 | 4 | /** 5 | * A CSS-in-JS style object. 6 | * 7 | * @public 8 | */ 9 | export interface CSSProperties extends Properties { 10 | [key: string]: CSSProperties | string | number | undefined | null; 11 | } 12 | 13 | function isPlainObject(value: unknown): value is CSSProperties { 14 | return typeof value === 'object' && value !== null && !Array.isArray(value); 15 | } 16 | 17 | /** 18 | * Merge the target object into the source. 19 | * 20 | * @public 21 | */ 22 | export function mergeCss( 23 | source: TSource, 24 | target: TTarget 25 | ): TSource { 26 | for (const key in target) { 27 | if (!target.hasOwnProperty(key) || ['__proto__', 'constructor', 'prototype'].includes(key)) { 28 | continue; 29 | } 30 | 31 | const targetVal = target[key]; 32 | const sourceVal = source[key]; 33 | 34 | if (isPlainObject(sourceVal) && isPlainObject(targetVal)) { 35 | source[key] = mergeCss(sourceVal, targetVal) as TSource[typeof key]; 36 | } else if (isPlainObject(targetVal)) { 37 | source[key] = mergeCss({}, targetVal) as TSource[typeof key]; 38 | } else { 39 | source[key] = targetVal as unknown as TSource[typeof key]; 40 | } 41 | } 42 | 43 | return source; 44 | } 45 | 46 | export type MergedCssDeclaration = { 47 | [K in keyof T | keyof U]: 48 | K extends keyof U 49 | ? U[K] 50 | : K extends keyof T 51 | ? T[K] 52 | : never; 53 | }; 54 | 55 | export type CssDeclaration = { 56 | css: T; 57 | toString(): string; 58 | }; 59 | 60 | export type UseCssDeclaration = (...args: T) => V; 61 | 62 | export type UseCss = { 63 | /** 64 | * Get the CSS declaration. 65 | */ 66 | (...args: T): CssDeclaration; 67 | 68 | /** 69 | * Merge the given CSS into the current definition. 70 | */ 71 | merge: ( 72 | fn: UseCssDeclaration 73 | ) => UseCss>; 74 | 75 | /** 76 | * Extend the current definition with the given CSS. 77 | */ 78 | extend: ( 79 | fn: UseCssDeclaration 80 | ) => UseCss>; 81 | } 82 | 83 | export function useCss( 84 | fn: UseCssDeclaration 85 | ): UseCss { 86 | const merges: UseCssDeclaration[] = []; 87 | 88 | function apply(...args: T): CssDeclaration { 89 | const cssObj = merges.reduce((carry, fn) => mergeCss(carry, fn(...args)), fn(...args)); 90 | return { 91 | css: cssObj, 92 | toString() { 93 | return css(cssObj); 94 | } 95 | }; 96 | } 97 | 98 | function merge( 99 | mergeFn: UseCssDeclaration 100 | ): UseCss> { 101 | merges.push(mergeFn); 102 | return apply as UseCss>; 103 | } 104 | 105 | function extend( 106 | extendFn: UseCssDeclaration 107 | ): UseCss> { 108 | return useCss>((...args) => { 109 | return mergeCss(apply(...args).css, extendFn(...args)) as MergedCssDeclaration; 110 | }); 111 | } 112 | 113 | (apply as UseCss).merge = merge; 114 | (apply as UseCss).extend = extend; 115 | 116 | return apply as UseCss; 117 | } -------------------------------------------------------------------------------- /test/helpers/date.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { pad, useDateFormats } from '../../src/helpers/date'; 3 | import { useDictionary } from '../../src/helpers/dictionary'; 4 | 5 | it('formats date strings', () => { 6 | const { define, format, unset } = useDateFormats(); 7 | 8 | const date = new Date(2000, 0, 1); 9 | 10 | expect(format(date, 'A')).toBe('AM'); 11 | expect(format(date, 'a')).toBe('am'); 12 | expect(format(date, 'Q')).toBe('1'); 13 | expect(format(date, 'YYYY')).toBe('2000'); 14 | expect(format(date, 'YY')).toBe('00'); 15 | expect(format(date, 'MMMM')).toBe('January'); 16 | expect(format(date, 'MMM')).toBe('Jan'); 17 | expect(format(date, 'MM')).toBe('01'); 18 | expect(format(date, 'M')).toBe('1'); 19 | expect(format(date, 'DDDD')).toBe('Saturday'); 20 | expect(format(date, 'DDD')).toBe('Sat'); 21 | expect(format(date, 'DD')).toBe('01'); 22 | expect(format(date, 'D')).toBe('1'); 23 | expect(format(date, 'HH')).toBe('00'); 24 | expect(format(date, 'H')).toBe('0'); 25 | expect(format(date, 'hh')).toBe('12'); 26 | expect(format(date, 'h')).toBe('12'); 27 | expect(format(date, 'mm')).toBe('00'); 28 | expect(format(date, 'm')).toBe('0'); 29 | expect(format(date, 'ss')).toBe('00'); 30 | expect(format(date, 's')).toBe('0'); 31 | expect(format(date, 'vvvv')).toBe('0000'); 32 | expect(format(date, 'vvv')).toBe('000'); 33 | expect(format(date, 'vv')).toBe('00'); 34 | expect(format(date, 'v')).toBe('0'); 35 | expect(format(date, '!')).toBe('!'); 36 | 37 | const date2 = new Date(2000, 0, 1, 13); 38 | 39 | expect(format(date2, 'A')).toBe('PM'); 40 | expect(format(date2, 'a')).toBe('pm'); 41 | expect(format(date2, 'HH')).toBe('13'); 42 | expect(format(date2, 'H')).toBe('13'); 43 | expect(format(date2, 'hh')).toBe('01'); 44 | expect(format(date2, 'h')).toBe('1'); 45 | 46 | define('AA', date => date.getHours() < 13 ? 'a.m.' : 'p.m.'); 47 | 48 | define({ 49 | 'AAA': date => date.getHours() < 13 ? 'A.M.' : 'P.M.', 50 | }); 51 | 52 | expect(format(date, 'AA')).toBe('a.m.'); 53 | expect(format(date, 'AAA')).toBe('A.M.'); 54 | 55 | unset('AA'); 56 | unset(['AAA']); 57 | 58 | expect(format(date, 'AA')).toBe('AMAM'); 59 | expect(format(date, 'AAA')).toBe('AMAMAM'); 60 | }); 61 | 62 | it('formats dates with a translate function', () => { 63 | // An english to spanish dictionary 64 | const { translate } = useDictionary({ 65 | 'January': 'Enero', 66 | 'February': 'Febrero', 67 | 'March': 'Marzo', 68 | 'April': 'Abril', 69 | 'May': 'Mayo', 70 | 'June': 'Junio', 71 | 'July': 'Julio', 72 | 'August': 'Agosto', 73 | 'September': 'Septiembre', 74 | 'October': 'Octubre', 75 | 'November': 'Noviembre', 76 | 'December': 'Diciembre' 77 | }); 78 | 79 | const { format } = useDateFormats({ 80 | translate 81 | }); 82 | 83 | const date = new Date(2000, 0, 1); 84 | 85 | expect(format(date, 'MMMM')).toBe('Enero'); 86 | }); 87 | 88 | it('formats dates with a dictionary', () => { 89 | // An english to spanish dictionary 90 | const { format } = useDateFormats({ 91 | translate: useDictionary({ 92 | 'January': 'Enero', 93 | 'February': 'Febrero', 94 | 'March': 'Marzo', 95 | 'April': 'Abril', 96 | 'May': 'Mayo', 97 | 'June': 'Junio', 98 | 'July': 'Julio', 99 | 'August': 'Agosto', 100 | 'September': 'Septiembre', 101 | 'October': 'Octubre', 102 | 'November': 'Noviembre', 103 | 'December': 'Diciembre' 104 | }) 105 | }); 106 | 107 | const date = new Date(2000, 0, 1); 108 | 109 | expect(format(date, 'MMMM')).toBe('Enero'); 110 | }); 111 | 112 | it('pads a digit to a specified length', () => { 113 | expect(pad(undefined, 1)).toBe(''); 114 | expect(pad(0, 1)).toBe('0'); 115 | expect(pad(0, 2)).toBe('00'); 116 | expect(pad(0, 3)).toBe('000'); 117 | }); -------------------------------------------------------------------------------- /bin/extractTypes.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs'; 2 | import { mkdir, rm, writeFile } from 'node:fs/promises'; 3 | import { resolve } from 'path'; 4 | import { Project, SourceFile } from "ts-morph"; 5 | 6 | const sourceFile: SourceFile = new Project({ 7 | tsConfigFilePath: './tsconfig.json' 8 | }).addSourceFileAtPath('./dist/index.d.ts'); 9 | 10 | const baseDir = resolve('./docs/types'); 11 | 12 | const extensions: Record = { 13 | 'PropertyDeclaration': '.prop.ts', 14 | 'PropertySignature': '.prop.ts', 15 | 'ClassDeclaration': '.class.ts', 16 | 'MemberDeclaration': '.member.ts', 17 | 'MethodDeclaration': '.method.ts', 18 | 'FunctionDeclaration': '.function.ts', 19 | 'TypeAliasDeclaration': '.type.ts', 20 | 'InterfaceDeclaration': '.interface.ts', 21 | 'VariableDeclaration': '.var.ts' 22 | }; 23 | 24 | const COMMENT_REGEX = /(export(\s+)?)?(declare(\s+?))?/; 25 | 26 | const EXPORT_REGEX = /export?(\s+)?(declare)(\s+)?/; 27 | 28 | function groupBy(array: T, key: string) { 29 | return array 30 | .reduce((hash, obj) => { 31 | // @ts-ignore 32 | if (obj[key] === undefined) return hash; 33 | 34 | // @ts-ignore 35 | return Object.assign(hash, { [obj[key]]:( hash[obj[key]] || [] ).concat(obj)}); 36 | }, {}); 37 | } 38 | 39 | export async function extractTypes() { 40 | if (existsSync(baseDir)) { 41 | await rm(baseDir, { 42 | recursive: true, 43 | force: true 44 | }); 45 | } 46 | 47 | await mkdir(baseDir); 48 | 49 | 50 | for (const [key, decorations] of sourceFile.getExportedDeclarations()) { 51 | const declarations: Record = {}; 52 | 53 | for (const decoration of decorations) { 54 | const kindName = decoration.getKindName(); 55 | 56 | if (kindName === 'VariableDeclaration') { 57 | continue; 58 | } 59 | 60 | if (!declarations[kindName]) { 61 | declarations[kindName] = []; 62 | } 63 | 64 | // if ('getProperties' in decoration) { 65 | // for (const prop of decoration.getProperties()) { 66 | // if (!declarations[prop.getKindName()]) { 67 | // declarations[prop.getKindName()] = []; 68 | // } 69 | 70 | // declarations[prop.getKindName()]!.push({ 71 | // filename: `${key}.${prop.getName()}`, 72 | // contents: prop 73 | // .getText() 74 | // .replace(COMMENT_REGEX, '') 75 | // }); 76 | // } 77 | // } 78 | 79 | // if ('getMethods' in decoration) { 80 | // for (const prop of decoration.getMethods()) { 81 | // if (!declarations[prop.getKindName()]) { 82 | // declarations[prop.getKindName()] = []; 83 | // } 84 | 85 | // declarations[prop.getKindName()]!.push({ 86 | // filename: `${key}.${prop.getName()}`, 87 | // contents: prop 88 | // .getText() 89 | // .replace(COMMENT_REGEX, '') 90 | // }); 91 | // } 92 | // } 93 | 94 | declarations[kindName].push({ 95 | filename: key, 96 | contents: decoration 97 | .getText() 98 | .replace(COMMENT_REGEX, '') 99 | .replace(EXPORT_REGEX, '') 100 | .trim() 101 | }); 102 | } 103 | 104 | for (const [kindName, files] of Object.entries(declarations)) { 105 | const groups = groupBy(files, 'filename'); 106 | 107 | for (const [key, value] of Object.entries(groups)) { 108 | // @ts-ignore 109 | const contents = value.map(({ contents }) => contents).join('\n'); 110 | const path = resolve(baseDir, `${key}${extensions[kindName] ?? '.ts'}`); 111 | 112 | await writeFile(path, contents); 113 | } 114 | } 115 | } 116 | } 117 | 118 | await extractTypes(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flipclock", 3 | "version": "1.0.0", 4 | "description": "A full featured, themeable, type safe, and well tested library for clocks, timers, counters, and flipboards.", 5 | "license": "MIT", 6 | "author": "objectivehtml", 7 | "contributors": [], 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/objectivehtml/flipclock.git" 11 | }, 12 | "homepage": "https://flipclockjs.com", 13 | "bugs": { 14 | "url": "https://github.com/objectivehtml/flipclock/issues" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "private": false, 20 | "sideEffects": false, 21 | "type": "module", 22 | "main": "./dist/FlipClock.umd.js", 23 | "module": "./dist/FlipClock.es.js", 24 | "types": "./dist/index.d.ts", 25 | "exports": { 26 | ".": { 27 | "types": "./dist/index.d.ts", 28 | "require": "./dist/FlipClock.umd", 29 | "import": "./dist/FlipClock.es.js", 30 | "default": "./dist/FlipClock.es.js" 31 | }, 32 | "./themes/flipclock": { 33 | "require": "./dist/themes/flipclock.css", 34 | "import": "./dist/themes/flipclock.css", 35 | "default": "./dist/themes/flipclock.css" 36 | } 37 | }, 38 | "typesVersions": {}, 39 | "lint-staged": { 40 | "*.{js,jsx,ts,tsx}": [ 41 | "eslint --fix" 42 | ] 43 | }, 44 | "scripts": { 45 | "dev": "vite serve dev", 46 | "build": "vite build", 47 | "postbuild": "pnpm tsup && pnpm extract-types && pnpm docs:build", 48 | "tsup": "tsup --dts-only", 49 | "test": "vitest", 50 | "peg": "npx peggy -o ./src/helpers/parser.js format.peg --dts --format es", 51 | "test:coverage": "vitest --coverage", 52 | "prepublishOnly": "pnpm build", 53 | "format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"dev/**/*.{js,ts,json,css,tsx,jsx}\"", 54 | "lint": "concurrently pnpm:lint:*", 55 | "lint:code": "eslint --ignore-path .gitignore --max-warnings 0 src/**/*.{ts,tsx}", 56 | "lint:types": "tsc --noEmit", 57 | "update-deps": "pnpm up -Li", 58 | "docs:dev": "vitepress dev docs", 59 | "docs:build": "vitepress build docs", 60 | "docs:preview": "vitepress preview docs", 61 | "extract-types": "node bin/extractTypes.ts", 62 | "changeset": "changeset", 63 | "changeset:version": "changeset version", 64 | "changeset:publish": "pnpm build && changeset publish", 65 | "prepare": "husky" 66 | }, 67 | "dependencies": { 68 | "date-fns": "^4.1.0", 69 | "goober": "^2.1.16", 70 | "solid-js": "^1.9.9" 71 | }, 72 | "devDependencies": { 73 | "@changesets/changelog-github": "^0.5.1", 74 | "@changesets/cli": "^2.29.6", 75 | "@commitlint/cli": "^19.8.1", 76 | "@commitlint/config-conventional": "^19.8.1", 77 | "@types/fs-extra": "^11.0.4", 78 | "@types/markdown-it": "^14.1.2", 79 | "@types/node": "^20.19.11", 80 | "@typescript-eslint/eslint-plugin": "^8.40.0", 81 | "@typescript-eslint/parser": "^8.40.0", 82 | "@vitest/coverage-v8": "^3.2.4", 83 | "@vuepress/bundler-vite": "2.0.0-rc.24", 84 | "@vuepress/plugin-markdown-include": "2.0.0-rc.112", 85 | "@vuepress/plugin-prismjs": "2.0.0-rc.112", 86 | "@vuepress/theme-default": "2.0.0-rc.112", 87 | "csstype": "^3.1.3", 88 | "date-fns": "^4.1.0", 89 | "esbuild": "^0.25.9", 90 | "eslint": "^8.57.1", 91 | "eslint-plugin-eslint-comments": "^3.2.0", 92 | "eslint-plugin-no-only-tests": "^3.3.0", 93 | "fs-extra": "^11.3.1", 94 | "goober": "^2.1.16", 95 | "husky": "^9.1.7", 96 | "jsdom": "^24.1.3", 97 | "lint-staged": "^16.1.5", 98 | "markdown-it": "^14.1.0", 99 | "markdown-it-async": "^2.2.0", 100 | "peggy": "^5.0.6", 101 | "prettier": "^3.6.2", 102 | "sass": "^1.90.0", 103 | "solid-js": "^1.9.9", 104 | "ts-morph": "^26.0.0", 105 | "tsup": "^8.5.0", 106 | "tsup-preset-solid": "^2.2.0", 107 | "typescript": "^5.9.2", 108 | "vite": "^7.1.3", 109 | "vite-plugin-dts": "^4.5.4", 110 | "vite-plugin-solid": "^2.11.8", 111 | "vitepress": "2.0.0-alpha.12", 112 | "vitest": "^3.2.4", 113 | "vue": "^3.5.19", 114 | "vuepress": "2.0.0-rc.24" 115 | }, 116 | "keywords": [ 117 | "flipclock", 118 | "flipboard", 119 | "clock", 120 | "elapsed time", 121 | "counter" 122 | ], 123 | "packageManager": "pnpm@9.1.1", 124 | "engines": { 125 | "node": ">=18", 126 | "pnpm": ">=9.0.0" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/helpers/duration.ts: -------------------------------------------------------------------------------- 1 | import { add, differenceInDays, differenceInHours, differenceInMinutes, differenceInMonths, differenceInSeconds, differenceInWeeks, differenceInYears, set, type Duration } from "date-fns"; 2 | import { pad } from "./date"; 3 | 4 | /** 5 | * The duration flag format function. 6 | * 7 | * @public 8 | */ 9 | export type DurationFlagFormatter = (duration: Duration, length: number) => string; 10 | 11 | /** 12 | * The duration map definition. 13 | * 14 | * @public 15 | */ 16 | export type DurationMapDefinition = [keyof Duration, DurationFlagFormatter] 17 | 18 | const unitConverters = { 19 | years: differenceInYears, 20 | months: differenceInMonths, 21 | weeks: differenceInWeeks, 22 | days: differenceInDays, 23 | hours: differenceInHours, 24 | minutes: differenceInMinutes, 25 | seconds: differenceInSeconds 26 | } as const; 27 | 28 | const unitAdders = { 29 | years: (date: Date, value: number) => add(date, { years: value }), 30 | months: (date: Date, value: number) => add(date, { months: value }), 31 | weeks: (date: Date, value: number) => add(date, { weeks: value }), 32 | days: (date: Date, value: number) => add(date, { days: value }), 33 | hours: (date: Date, value: number) => add(date, { hours: value }), 34 | minutes: (date: Date, value: number) => add(date, { minutes: value }), 35 | seconds: (date: Date, value: number) => add(date, { seconds: value }) 36 | } as const; 37 | 38 | /** 39 | * Get duration between two dates with only specified keys 40 | * 41 | * @public 42 | */ 43 | export function getFilteredDuration( 44 | start: Date | number, 45 | end: Date | number, 46 | keys: T[] 47 | ): Pick { 48 | return keys 49 | .sort((a, b) => Object.keys(unitConverters).indexOf(a as string) - Object.keys(unitConverters).indexOf(b as string)) 50 | .reduce((carry, key) => { 51 | const converter = unitConverters[key as keyof typeof unitConverters]; 52 | const adder = unitAdders[key as keyof typeof unitAdders]; 53 | const value = converter(carry.end, carry.start); 54 | 55 | return { 56 | ...carry, 57 | result: { ...carry.result, [key]: value }, 58 | start: adder(carry.start, value) 59 | }; 60 | }, { 61 | result: {} as Pick, 62 | start: new Date(start), 63 | end: new Date(end) 64 | }).result; 65 | } 66 | 67 | /** 68 | * The return type for `useDurationFormats()`. 69 | * 70 | * @public 71 | */ 72 | export type UseDurationFormats = { 73 | /** 74 | * Format the start and end date into a string. 75 | */ 76 | format: (start: Date, end: Date, format: string) => string 77 | } 78 | 79 | export function useDurationFormats(): UseDurationFormats { 80 | const flags: Record = { 81 | years: 'Y', 82 | months: 'M', 83 | weeks: 'W', 84 | days: 'D', 85 | hours: 'h', 86 | minutes: 'm', 87 | seconds: 's' 88 | }; 89 | 90 | function format(start: Date, end: Date, format: string) { 91 | const flagPattern: RegExp = new RegExp( 92 | Object.values(flags).map(value => `${value}+`).join('|'), 'g' 93 | ); 94 | 95 | const durationKeys = format.match(flagPattern) 96 | ?.map(value => value[0]) 97 | .filter((value, index, self) => self.indexOf(value) === index) 98 | .reduce<(keyof Duration)[]>((carry, flag) => { 99 | const match = Object.keys(flags).find( 100 | (key) => flags[key as keyof Duration] === flag 101 | ) as keyof Duration; 102 | 103 | return [...carry, match]; 104 | }, [])!; 105 | 106 | const duration = getFilteredDuration( 107 | set(new Date(start), {milliseconds: 0}), 108 | set(new Date(end), {milliseconds: 0}), 109 | durationKeys 110 | ); 111 | 112 | const entries = Object.entries(duration) as [keyof Duration, number][]; 113 | 114 | return entries.reduce((carry, [key, value]) => { 115 | const pattern = new RegExp(`${flags[key]}+`, 'g'); 116 | 117 | return carry.replace(pattern, flag => { 118 | return pad(Math.abs(value), flag.length); 119 | }); 120 | }, format); 121 | } 122 | 123 | return { 124 | format 125 | }; 126 | } -------------------------------------------------------------------------------- /test/faces/ElapsedTime.test.ts: -------------------------------------------------------------------------------- 1 | import { add, set, sub } from "date-fns"; 2 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 3 | import { nextTick } from ".."; 4 | import { flipClock } from "../../src/FlipClock"; 5 | import { elapsedTime } from "../../src/faces"; 6 | import { useDurationFormats } from "../../src/helpers/duration"; 7 | import { theme } from "../../src/themes/flipclock"; 8 | 9 | describe('ElapsedTime', () => { 10 | beforeEach(() => { 11 | vi.useFakeTimers(); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.useRealTimers(); 16 | }); 17 | 18 | it('creates an elapsed time to a given date', () => { 19 | const date = new Date; 20 | 21 | const instance = flipClock({ 22 | parent: document.createElement('div'), 23 | face: elapsedTime({ 24 | to: add(date, { seconds: 3 }), 25 | format: '[mm]:[ss]' 26 | }), 27 | theme: theme() 28 | }); 29 | 30 | expect(instance.face.formattedString).toBe('[00]:[03]'); 31 | 32 | nextTick(instance); 33 | 34 | expect(instance.face.formattedString).toBe('[00]:[02]'); 35 | 36 | nextTick(instance); 37 | 38 | expect(instance.face.formattedString).toBe('[00]:[01]'); 39 | 40 | nextTick(instance); 41 | 42 | expect(instance.face.formattedString).toBe('[00]:[00]'); 43 | 44 | nextTick(instance); 45 | 46 | expect(instance.face.formattedString).toBe('[00]:[00]'); 47 | expect(instance.timer.isStopped).toBeTruthy(); 48 | }); 49 | 50 | it('creates an elapsed time from a date in the past', () => { 51 | const date = new Date; 52 | 53 | const instance = flipClock({ 54 | parent: document.createElement('div'), 55 | face: elapsedTime({ 56 | from: sub(date, { minutes: 60 }), 57 | format: '[mm]:[ss]' 58 | }), 59 | theme: theme() 60 | }); 61 | 62 | expect(instance.face.formattedString).toBe('[60]:[00]'); 63 | 64 | nextTick(instance); 65 | 66 | expect(instance.face.formattedString).toBe('[60]:[01]'); 67 | 68 | nextTick(instance); 69 | 70 | expect(instance.face.formattedString).toBe('[60]:[02]'); 71 | }); 72 | 73 | it('creates an elapsed time from a date in the future', () => { 74 | const date = new Date; 75 | 76 | const instance = flipClock({ 77 | parent: document.createElement('div'), 78 | face: elapsedTime({ 79 | from: add(date, { minutes: 60 }), 80 | format: '[mm]:[ss]' 81 | }), 82 | theme: theme() 83 | }); 84 | 85 | expect(instance.face.formattedString).toBe('[60]:[00]'); 86 | 87 | nextTick(instance); 88 | 89 | expect(instance.face.formattedString).toBe('[59]:[59]'); 90 | 91 | nextTick(instance); 92 | 93 | expect(instance.face.formattedString).toBe('[59]:[58]'); 94 | }); 95 | 96 | it('creates an elapsed time from and to a given date', () => { 97 | const from = set(new Date(), { milliseconds: 0 }); 98 | 99 | const instance = flipClock({ 100 | parent: document.createElement('div'), 101 | face: elapsedTime({ 102 | from, 103 | to: add(from, { seconds: 3 }), 104 | format: '[mm]:[ss]' 105 | }), 106 | theme: theme() 107 | }); 108 | 109 | expect(instance.face.formattedString).toBe('[00]:[00]'); 110 | 111 | nextTick(instance); 112 | 113 | expect(instance.face.formattedString).toBe('[00]:[01]'); 114 | 115 | nextTick(instance); 116 | 117 | expect(instance.face.formattedString).toBe('[00]:[02]'); 118 | 119 | nextTick(instance); 120 | 121 | expect(instance.face.formattedString).toBe('[00]:[03]'); 122 | 123 | nextTick(instance); 124 | 125 | expect(instance.face.formattedString).toBe('[00]:[03]'); 126 | expect(instance.timer.isStopped).toBeTruthy(); 127 | }); 128 | 129 | it('creates an elapsed time with a formatter', () => { 130 | const date = new Date; 131 | 132 | const instance = flipClock({ 133 | face: elapsedTime({ 134 | format: '[mm]:[ss]', 135 | formatter: useDurationFormats() 136 | }), 137 | theme: theme() 138 | }); 139 | 140 | expect(instance.face.faceValue().value).toBe( 141 | instance.face.formatter.format(date, date, instance.face.format) 142 | ); 143 | }); 144 | }); -------------------------------------------------------------------------------- /src/Timer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Timer class uses a requestAnimationFrame loop to build a timer that can 3 | * start and stop. 4 | * 5 | * @public 6 | */ 7 | export class Timer { 8 | 9 | /** 10 | * The count increments with each interval. 11 | * 12 | * @protected 13 | */ 14 | protected $count: number = 0; 15 | 16 | /** 17 | * The requestAnimationFrame handle number. 18 | * 19 | * @protected 20 | */ 21 | protected $handle?: number; 22 | 23 | /** 24 | * The number of milliseconds that define an interval. 25 | * 26 | * @public 27 | */ 28 | public readonly interval: number; 29 | 30 | /** 31 | * The timestamp of the last loop. 32 | * 33 | * @protected 34 | */ 35 | protected $lastLoop?: number; 36 | 37 | /** 38 | * The date the timer starts. 39 | * 40 | * @protected 41 | */ 42 | protected $startDate?: Date; 43 | 44 | /** 45 | * Construct the timer. 46 | * 47 | * @public 48 | */ 49 | constructor(ms: number = 1000) { 50 | this.interval = ms; 51 | } 52 | 53 | /** 54 | * Get the number of times the timer has ticked. 55 | * 56 | * @public 57 | */ 58 | get count(): number { 59 | return this.$count; 60 | } 61 | 62 | /** 63 | * The `elapsed` attribute. 64 | * 65 | * @public 66 | */ 67 | get elapsed(): number { 68 | if (!this.$startDate) { 69 | return 0; 70 | } 71 | 72 | return Math.max(0, Date.now() - this.$startDate.getTime()); 73 | } 74 | 75 | /** 76 | * The `elapsedSinceLastLoop` attribute. 77 | * 78 | * @public 79 | */ 80 | get elapsedSinceLastLoop(): number { 81 | if (!this.lastLoop) { 82 | return 0; 83 | } 84 | 85 | return Date.now() - this.lastLoop; 86 | } 87 | 88 | /** 89 | * Determines if the Timer is currently running. 90 | * 91 | * @public 92 | */ 93 | get isRunning(): boolean { 94 | return this.$handle !== undefined; 95 | } 96 | 97 | /** 98 | * Determines if the Timer is currently stopped. 99 | * 100 | * @public 101 | */ 102 | get isStopped(): boolean { 103 | return !this.isRunning; 104 | } 105 | 106 | /** 107 | * Get the last timestamp the timer looped. 108 | * 109 | * @public 110 | */ 111 | get lastLoop(): number { 112 | return this.$lastLoop || 0; 113 | } 114 | 115 | /** 116 | * Set the last timestamp the timer looped. 117 | * 118 | * @public 119 | */ 120 | set lastLoop(value: number) { 121 | this.$lastLoop = value; 122 | } 123 | 124 | /** 125 | * Get the date object when the timer started. 126 | * 127 | * @public 128 | */ 129 | get started(): Date | undefined { 130 | return this.$startDate; 131 | } 132 | 133 | /** 134 | * Resets the timer. If a callback is provided, re-start the clock. 135 | * 136 | * @public 137 | */ 138 | reset(fn?: (timer: Timer) => void): Timer { 139 | this.stop(() => { 140 | this.$count = 0; 141 | this.$lastLoop = 0; 142 | this.start(fn); 143 | }); 144 | 145 | return this; 146 | } 147 | 148 | /** 149 | * Starts the timer. 150 | * 151 | * @public 152 | */ 153 | start(fn?: (timer: Timer) => void): Timer { 154 | this.$startDate = new Date; 155 | this.$lastLoop = this.$startDate.getTime(); 156 | 157 | const loop = () => { 158 | if (Date.now() - this.lastLoop >= this.interval) { 159 | if (typeof fn === 'function') { 160 | fn(this); 161 | } 162 | 163 | this.$lastLoop = Date.now(); 164 | this.$count++; 165 | } 166 | 167 | this.$handle = requestAnimationFrame(loop); 168 | 169 | return this; 170 | }; 171 | 172 | return loop(); 173 | } 174 | 175 | /** 176 | * Stops the timer. 177 | * 178 | * @public 179 | */ 180 | stop(fn?: (timer: Timer) => void): Timer { 181 | if (this.isRunning && this.$handle) { 182 | window.cancelAnimationFrame(this.$handle); 183 | 184 | this.$lastLoop = 0; 185 | this.$handle = undefined; 186 | 187 | if (typeof fn === 'function') { 188 | fn(this); 189 | } 190 | } 191 | 192 | return this; 193 | } 194 | } 195 | 196 | /** 197 | * Create a new `Timer` instance. 198 | * 199 | * @public 200 | */ 201 | export function timer(interval: number = 1000) { 202 | return new Timer(interval); 203 | } 204 | -------------------------------------------------------------------------------- /test/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "solid-js"; 2 | import { render } from "solid-js/web"; 3 | import { vi } from "vitest"; 4 | import type { Face, FaceHooks } from "../src/Face"; 5 | import { faceValue, type FaceValue } from "../src/FaceValue"; 6 | import type { DisposeFunction, FlipClock, Theme } from "../src/FlipClock"; 7 | 8 | export function nextTick(instance: FlipClock) { 9 | vi.advanceTimersToNextFrame(); 10 | vi.advanceTimersByTime(instance.timer.interval); 11 | vi.advanceTimersToNextFrame(); 12 | vi.advanceTimersToNextFrame(); 13 | } 14 | 15 | export function createHookMocks(): Required> { 16 | return { 17 | afterCreate: vi.fn(), 18 | beforeMount: vi.fn(), 19 | afterMount: vi.fn(), 20 | beforeUnmount: vi.fn(), 21 | afterUnmount: vi.fn(), 22 | beforeInterval: vi.fn(), 23 | afterInterval: vi.fn(), 24 | beforeStart: vi.fn(), 25 | afterStart: vi.fn(), 26 | beforeStop: vi.fn(), 27 | afterStop: vi.fn(), 28 | }; 29 | } 30 | 31 | export class SimpleCounter implements Face { 32 | public value: FaceValue; 33 | public hooks: Required>; 34 | 35 | constructor(value: number) { 36 | this.hooks = createHookMocks(); 37 | this.value = faceValue(value); 38 | } 39 | 40 | faceValue(): FaceValue { 41 | return this.value; 42 | } 43 | 44 | interval(): void { 45 | this.value.value++; 46 | } 47 | 48 | afterCreate(instance: FlipClock): void { 49 | this.hooks.afterCreate(instance); 50 | } 51 | 52 | beforeMount(instance: FlipClock): void { 53 | this.hooks.beforeMount(instance); 54 | } 55 | 56 | afterMount(instance: FlipClock): void { 57 | this.hooks.afterMount(instance); 58 | } 59 | 60 | beforeUnmount(instance: FlipClock): void { 61 | this.hooks.beforeUnmount(instance); 62 | } 63 | 64 | afterUnmount(instance: FlipClock): void { 65 | this.hooks.afterUnmount(instance); 66 | } 67 | 68 | beforeInterval(instance: FlipClock): void { 69 | this.hooks.beforeInterval(instance); 70 | } 71 | 72 | afterInterval(instance: FlipClock): void { 73 | this.hooks.afterInterval(instance); 74 | } 75 | 76 | beforeStart(instance: FlipClock): void { 77 | this.hooks.beforeStart(instance); 78 | } 79 | 80 | afterStart(instance: FlipClock): void { 81 | this.hooks.afterStart(instance); 82 | } 83 | 84 | beforeStop(instance: FlipClock): void { 85 | this.hooks.beforeStop(instance); 86 | } 87 | 88 | afterStop(instance: FlipClock): void { 89 | this.hooks.afterStop(instance); 90 | } 91 | } 92 | 93 | export function createSimpleTheme(): [Required>, Theme] { 94 | const hooks = createHookMocks(); 95 | 96 | const theme = { 97 | render>(el: Element, instance: FlipClock): [Element, DisposeFunction] { 98 | return createRoot(dispose => { 99 | let node: HTMLDivElement|undefined; 100 | 101 | render(() => ( 102 |
103 | {instance.face.faceValue().value} 104 |
105 | ), el); 106 | 107 | return [node!, dispose]; 108 | }); 109 | }, 110 | 111 | afterCreate(instance: FlipClock): void { 112 | hooks.afterCreate(instance); 113 | }, 114 | 115 | beforeMount(instance: FlipClock): void { 116 | hooks.beforeMount(instance); 117 | }, 118 | 119 | afterMount(instance: FlipClock): void { 120 | hooks.afterMount(instance); 121 | }, 122 | 123 | beforeUnmount(instance: FlipClock): void { 124 | hooks.beforeUnmount(instance); 125 | }, 126 | 127 | afterUnmount(instance: FlipClock): void { 128 | hooks.afterUnmount(instance); 129 | }, 130 | 131 | beforeInterval(instance: FlipClock): void { 132 | hooks.beforeInterval(instance); 133 | }, 134 | 135 | afterInterval(instance: FlipClock): void { 136 | hooks.afterInterval(instance); 137 | }, 138 | 139 | beforeStart(instance: FlipClock): void { 140 | hooks.beforeStart(instance); 141 | }, 142 | 143 | afterStart(instance: FlipClock): void { 144 | hooks.afterStart(instance); 145 | }, 146 | 147 | beforeStop(instance: FlipClock): void { 148 | hooks.beforeStop(instance); 149 | }, 150 | 151 | afterStop(instance: FlipClock): void { 152 | hooks.afterStop(instance); 153 | } 154 | }; 155 | 156 | return [hooks, theme]; 157 | } -------------------------------------------------------------------------------- /test/helpers/charset.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { characterRange, defaultCharset, fisherYatesShuffle, range, useCharset } from '../../src/helpers/charset'; 3 | 4 | it('validates whitelisted and blacklisted characters', () => { 5 | const { isBlacklisted, isWhitelisted } = useCharset({ 6 | whitelist: ['@'], 7 | blacklist: ['#'] 8 | }); 9 | 10 | expect(isWhitelisted('@')).toEqual(true); 11 | expect(isBlacklisted('#')).toEqual(true); 12 | }); 13 | 14 | it('retrieves chunks of the charset going forwards and backwards', () => { 15 | const { charset, chunk } = useCharset(); 16 | 17 | expect(chunk('@', 1)).toStrictEqual([' ']); 18 | expect(chunk('A', 1000)).toHaveLength(charset.length + 1); 19 | expect(chunk(':', 7)).toStrictEqual(['-', '.', ',', '!', '?', ' ', 'a']); 20 | expect(chunk(' ', 2)).toStrictEqual(['a', 'b']); 21 | expect(chunk(undefined, 1)).toStrictEqual(['a']); 22 | expect(chunk(undefined, 5)).toStrictEqual(['a', 'b', 'c', 'd', 'e']); 23 | expect(chunk(undefined, 1000)).toHaveLength(charset.length + 1); 24 | 25 | expect(chunk('@', -1)).toStrictEqual([' ']); 26 | expect(chunk('A', -1000)).toHaveLength(charset.length + 1); 27 | expect(chunk('"', -7)).toStrictEqual(['9', '8', '7', '6', '5', '4', '3']); 28 | expect(chunk(' ', -2)).toStrictEqual(['?', '!']); 29 | expect(chunk(undefined, -1)).toStrictEqual(['?']); 30 | expect(chunk(undefined, -5)).toStrictEqual(['?', '!', ',', '.', '-']); 31 | expect(chunk(undefined, -1000)).toHaveLength(charset.length + 1); 32 | }); 33 | 34 | it('retrieves the next value towards the target', () => { 35 | const { next, prev } = useCharset({ 36 | blacklist: ['#'], 37 | whitelist: ['@'] 38 | }); 39 | 40 | expect(next('a')).toBe('b'); 41 | expect(next('b')).toBe('c'); 42 | expect(next(' ')).toBeUndefined(); 43 | expect(next(undefined, 'a')).toBe('a'); 44 | expect(next(undefined, 'a', 2)).toBe('a'); 45 | expect(next('9', '!')).toBe('"'); 46 | expect(next('9', '!', 2)).toBe('\''); 47 | expect(next('a', '!', 100)).toBe('!'); 48 | expect(next('@')).toBe('@'); 49 | expect(next('@', '@')).toBe('@'); 50 | expect(next('#', '!')).toBe('"'); 51 | expect(next('a', 'z')).toBe('b'); 52 | expect(next('a', 'z', 5)).toBe('f'); 53 | expect(next('a', 'z', 100)).toBe('z'); 54 | expect(next('!', 'a', 2)).toBe(' '); 55 | expect(next('!', '?', 2)).toBe('?'); 56 | expect(next('!', '?')).toBe('?'); 57 | expect(next('?', undefined)).toBeUndefined(); 58 | 59 | expect(prev('b')).toBe('a'); 60 | expect(prev('c')).toBe('b'); 61 | expect(prev('a')).toBe(' '); 62 | expect(prev(' ')).toBeUndefined(); 63 | expect(prev('a', undefined, 2)).toBeUndefined(); 64 | expect(prev(undefined, '?')).toBe('?'); 65 | expect(prev(undefined, '?', 2)).toBe('?'); 66 | expect(prev('?', '.')).toBe('!'); 67 | expect(prev('?', '.', 2)).toBe(','); 68 | expect(prev('?', '.', 100)).toBe('.'); 69 | expect(prev('!', '!')).toBe('!'); 70 | expect(prev('@', '@')).toBe('@'); 71 | expect(prev('#', '!')).toBe('"'); 72 | expect(prev('z', 'a')).toBe('y'); 73 | expect(prev('z', 'a', 5)).toBe('u'); 74 | expect(prev('z', 'a', 100)).toBe('a'); 75 | }); 76 | 77 | it('using a randomize charset', () => { 78 | const { chunk } = useCharset({ 79 | shuffle: true 80 | }); 81 | 82 | expect(chunk('a', 5)).not.toStrictEqual(defaultCharset); 83 | }); 84 | 85 | it('using a custom shuffle function', () => { 86 | const { charset } = useCharset(); 87 | 88 | const { chunk } = useCharset({ 89 | shuffle: fisherYatesShuffle 90 | }); 91 | 92 | expect(chunk('a', 100)).not.toStrictEqual(charset); 93 | }); 94 | 95 | it('using a custom charset', () => { 96 | const { chunk } = useCharset({ 97 | emptyChar: '$', 98 | charset: () => ['a', 'b', 'c', 'd', 'e', 'f'] 99 | }); 100 | 101 | expect(chunk('a', 1)).toStrictEqual(['b']); 102 | expect(chunk('a', 5)).toStrictEqual(['b', 'c', 'd', 'e', 'f']); 103 | expect(chunk('a', 100)).toStrictEqual(['b', 'c', 'd', 'e', 'f', '$', 'a']); 104 | }); 105 | 106 | it('the default charset', () => { 107 | const { charset } = useCharset(); 108 | 109 | expect(charset).toEqual(defaultCharset()); 110 | }); 111 | 112 | it('creating character ranges', () => { 113 | expect(characterRange('a', 'b')).toEqual(['a', 'b']); 114 | expect(characterRange('a', 'c')).toEqual(['a', 'b', 'c']); 115 | expect(characterRange('a', 'z')).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']); 116 | }); 117 | 118 | it('creating ranges', () => { 119 | expect(range(5, 1)).toEqual([5]); 120 | expect(range(5, 2)).toEqual([5, 6]); 121 | expect(range(5, 5)).toEqual([5, 6, 7, 8, 9]); 122 | expect(range(5, 10)).toEqual([5, 6, 7, 8, 9, 10, 11, 12, 13, 14]); 123 | }); -------------------------------------------------------------------------------- /test/faces/Alphanumeric.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { nextTick } from ".."; 3 | import { alphanumeric } from "../../src/faces"; 4 | import { faceValue } from "../../src/FaceValue"; 5 | import { flipClock } from "../../src/FlipClock"; 6 | import { useSequencer } from "../../src/helpers/sequencer"; 7 | import { theme } from "../../src/themes/flipclock"; 8 | 9 | describe('Alphanumeric', () => { 10 | beforeEach(() => { 11 | vi.useFakeTimers(); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.useRealTimers(); 16 | }); 17 | 18 | it('creates an alphanumeric that ticks from a to z', () => { 19 | const instance = flipClock({ 20 | autoStart: false, 21 | face: alphanumeric({ 22 | value: faceValue('a'), 23 | targetValue: faceValue('z') 24 | }), 25 | theme: theme() 26 | }); 27 | 28 | expect(instance.face.faceValue().value).toStrictEqual(['a']); 29 | 30 | instance.face.increment(); 31 | 32 | expect(instance.face.faceValue().value).toStrictEqual(['b']); 33 | }); 34 | 35 | it('creates an alphanumeric that skips 2 characters', () => { 36 | const instance = flipClock({ 37 | autoStart: false, 38 | face: alphanumeric({ 39 | skipChars: 2, 40 | value: faceValue('a'), 41 | targetValue: faceValue('z') 42 | }), 43 | theme: theme() 44 | }); 45 | 46 | expect(instance.face.faceValue().value).toStrictEqual(['a']); 47 | 48 | instance.face.increment(); 49 | 50 | expect(instance.face.faceValue().value).toStrictEqual(['c']); 51 | }); 52 | 53 | it('creates an alphanumeric that ticks forwards', () => { 54 | const instance = flipClock({ 55 | autoStart: false, 56 | face: alphanumeric({ 57 | direction: 'forwards', 58 | value: faceValue('aa'), 59 | targetValue: faceValue('bb') 60 | }), 61 | theme: theme() 62 | }); 63 | 64 | expect(instance.face.faceValue().value).toStrictEqual(['a', 'a']); 65 | 66 | instance.face.increment(); 67 | 68 | expect(instance.face.faceValue().value).toStrictEqual(['b', 'b']); 69 | }); 70 | 71 | it('creates an alphanumeric that ticks backwards', () => { 72 | const instance = flipClock({ 73 | autoStart: false, 74 | face: alphanumeric({ 75 | direction: 'backwards', 76 | value: faceValue('bb'), 77 | targetValue: faceValue('aa') 78 | }), 79 | theme: theme() 80 | }); 81 | 82 | expect(instance.face.faceValue().value).toStrictEqual(['b', 'b']); 83 | 84 | instance.face.decrement(); 85 | 86 | expect(instance.face.faceValue().value).toStrictEqual(['a', 'a']); 87 | }); 88 | 89 | it('uses a custom sequencer', () => { 90 | const instance = flipClock({ 91 | autoStart: false, 92 | face: alphanumeric({ 93 | sequencer: useSequencer(), 94 | value: faceValue('a'), 95 | targetValue: faceValue('z') 96 | }), 97 | theme: theme() 98 | }); 99 | 100 | expect(instance.face.faceValue().value).toStrictEqual(['a']); 101 | 102 | instance.face.increment(); 103 | 104 | expect(instance.face.faceValue().value).toStrictEqual(['b']); 105 | }); 106 | 107 | it('increments on an interval', () => { 108 | const instance = flipClock({ 109 | parent: document.createElement('div'), 110 | face: alphanumeric({ 111 | method: 'increment', 112 | value: faceValue('a'), 113 | targetValue: faceValue('c') 114 | }), 115 | theme: theme() 116 | }); 117 | 118 | expect(instance.face.faceValue().value).toStrictEqual(['a']); 119 | 120 | nextTick(instance); 121 | 122 | expect(instance.face.faceValue().value).toStrictEqual(['b']); 123 | 124 | nextTick(instance); 125 | 126 | expect(instance.timer.isStopped).toBeTruthy(); 127 | expect(instance.face.faceValue().value).toStrictEqual(['c']); 128 | 129 | nextTick(instance); 130 | 131 | expect(instance.face.faceValue().value).toStrictEqual(['c']); 132 | }); 133 | 134 | it('does not have a target value', () => { 135 | const instance = flipClock({ 136 | parent: document.createElement('div'), 137 | face: alphanumeric({ 138 | method: 'increment', 139 | value: faceValue('a') 140 | }), 141 | theme: theme() 142 | }); 143 | 144 | expect(instance.face.faceValue().value).toStrictEqual(['a']); 145 | 146 | nextTick(instance); 147 | 148 | expect(instance.timer.isStopped).toBeTruthy(); 149 | expect(instance.face.faceValue().value).toStrictEqual(['a']); 150 | }); 151 | }); --------------------------------------------------------------------------------