Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
root = true

[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
indent_style = tab
indent_size = 2

[*.js]
indent_style = tab
indent_size = 2
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "3.5.1",
"version": "3.5.2",
"private": true,
"engines": {
"node": ">=20.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "3.5.1",
"version": "3.5.2",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
8 changes: 4 additions & 4 deletions packages/library/src/common/international.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { memoizeFunction } from '#library/function.library.js';
import { isFunction, isDefined } from '#library/assertion.library.js';

/** memoized helper for Intl.RelativeTimeFormat instances */
const getRTF = memoizeFunction((locale?: string, style: Intl.RelativeTimeFormatStyle = 'narrow') => {
return new Intl.RelativeTimeFormat(locale, { style });
const getRTF = memoizeFunction((locale?: string, style: Intl.RelativeTimeFormatStyle = 'narrow', numeric: Intl.RelativeTimeFormatNumeric = 'always') => {
return new Intl.RelativeTimeFormat(locale, { style, numeric });
});

/** memoized helper for Intl.ListFormat instances */
Expand Down Expand Up @@ -63,9 +63,9 @@ export function canonicalLocale(locale: string): string | undefined {
}

/** return a localized relative time string (e.g., 'in 2 days') */
export function getRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, locale?: string, style: Intl.RelativeTimeFormatStyle = 'narrow') {
export function getRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, locale?: string, style: Intl.RelativeTimeFormatStyle = 'narrow', numeric: Intl.RelativeTimeFormatNumeric = 'always') {
try {
return getRTF(locale, style).format(value, unit);
return getRTF(locale, style, numeric).format(value, unit);
} catch (e) {
return `${value} ${unit}`;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default defineConfig({
{ text: 'Modularity', link: '/doc/tempo.modularity' },
{ text: 'Terms Plugins', link: '/doc/tempo.plugin' },
{ text: 'Extension Plugins', link: '/doc/tempo.extension' },
{ text: 'Premium Plugins ↗', link: 'https://magmacomputing.github.io/tempo-plugin-docs/' },
{ text: 'Plugin Ecosystem', link: '/doc/ecosystem' }
]
},
{
Expand Down
201 changes: 201 additions & 0 deletions packages/tempo/.vitepress/theme/components/CatalogList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';

interface Plugin {
id: string;
name: string;
description: string;
packageName: string;
price: number;
status: string;
}

const plugins = ref<Plugin[]>([])
const loading = ref(true)

onMounted(async () => {
try {
const res = await fetch('https://firestore.googleapis.com/v1/projects/tempo-registry/databases/(default)/documents/catalog');
const data = await res.json();

if (data.documents) {
plugins.value = data.documents.map((doc: any) => {
const id = doc.name.split('/').pop();
const fields = doc.fields;
return {
id,
name: fields.name?.stringValue || '',
description: fields.description?.stringValue || '',
packageName: fields.packageName?.stringValue || '',
price: parseInt(fields.price?.integerValue || '0'),
status: fields.status?.stringValue || 'active'
}
})
}
} catch (e) {
console.error('Failed to fetch catalog', e);
} finally {
loading.value = false;
}
})

const communityPlugins = computed(() => plugins.value.filter(p => p.price === 0 && p.status === 'active'));
const premiumPlugins = computed(() => plugins.value.filter(p => p.price > 0 && p.status === 'active'));
const comingSoonPlugins = computed(() => plugins.value.filter(p => p.status === 'coming_soon'));

const copyInstall = (pkgName: string) => {
navigator.clipboard.writeText(`npm install ${pkgName}`);
alert(`Copied: npm install ${pkgName}`);
}
</script>

<template>
<div class="catalog-container">
<div v-if="loading" class="loading">Loading catalog...</div>

<div v-else>
<h2 id="community">Community Plugins</h2>
<p>These plugins are free, open-source extensions that do not require a license token.</p>
<div class="grid">
<div v-for="plugin in communityPlugins" :key="plugin.id" class="card">
<h3>{{ plugin.name }}</h3>
<p>{{ plugin.description }}</p>
<div class="actions" style="display: flex; gap: 0.5rem; align-items: center;">
<code>npm install {{ plugin.packageName }}</code>
<button @click="copyInstall(plugin.packageName)" class="btn icon-btn" title="Copy install command">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</button>
<a :href="`https://www.npmjs.com/package/${plugin.packageName}`" target="_blank" class="btn btn-secondary icon-btn" title="View Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
</a>
</div>
</div>
</div>

<h2 id="premium">Premium Plugins</h2>
<p>Enterprise-grade extensions. A cryptographic license token is required.</p>
<div class="grid">
<div v-for="plugin in premiumPlugins" :key="plugin.id" class="card premium-card">
<div class="badge">Premium</div>
<h3>{{ plugin.name }}</h3>
<p>{{ plugin.description }}</p>
<div class="actions" style="display: flex; gap: 0.5rem; align-items: center;">
<a href="https://registry.magmacomputing.com.au" target="_blank" class="btn primary" style="flex: 1;">Get a License</a>
<a :href="`https://www.npmjs.com/package/${plugin.packageName}`" target="_blank" class="btn btn-secondary icon-btn" title="View Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
</a>
</div>
</div>
</div>

<h2 id="coming-soon" v-if="comingSoonPlugins.length > 0">Coming Soon</h2>
<div class="grid">
<div v-for="plugin in comingSoonPlugins" :key="plugin.id" class="card disabled">
<h3>{{ plugin.name }}</h3>
<p>{{ plugin.description }}</p>
</div>
</div>
</div>
</div>
</template>

<style scoped>
.catalog-container {
margin-top: 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1.5rem;
background-color: var(--vp-c-bg-soft);
position: relative;
display: flex;
flex-direction: column;
}
.card h3 {
margin-top: 0;
margin-bottom: 0.5rem;
font-size: 1.25rem;
}
.card p {
flex-grow: 1;
font-size: 0.95rem;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
}
.actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.actions code {
flex-grow: 1;
font-size: 0.85em;
padding: 0.4rem 0.6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn {
background-color: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.8rem;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
text-decoration: none;
text-align: center;
}
.icon-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.4rem;
}
.btn:hover {
background-color: var(--vp-c-brand-dark);
}
.btn.primary {
width: 100%;
}
.premium-card {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.badge {
position: absolute;
top: -10px;
right: 15px;
background: var(--vp-c-brand);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: bold;
}
.btn-secondary {
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.btn-secondary:hover {
background-color: var(--vp-c-bg-mute);
}
.disabled {
opacity: 0.6;
}
.loading {
text-align: center;
padding: 3rem;
font-size: 1.2rem;
color: var(--vp-c-text-2);
}
</style>
10 changes: 10 additions & 0 deletions packages/tempo/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import DefaultTheme from 'vitepress/theme'
import type { Theme } from 'vitepress'
import CatalogList from './components/CatalogList.vue'

export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('CatalogList', CatalogList)
}
} satisfies Theme
9 changes: 9 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.5.2]

### Added
- **Minified Global Bundles**: The build pipeline now natively produces highly optimized, minified IIFE bundles (`*.min.js`) for both Tempo Core and all Community Plugins, significantly reducing payload size for developers using CDN `<script>` tags.
- **Unified Global Namespace**: Re-architected the browser-global export strategy. Both the core library and all `<script>` tag plugins now elegantly attach to a single, collision-free `window.Magma` namespace (e.g., `window.Magma.Tempo` and `window.Magma.plugins.astro`), dramatically improving developer experience and eliminating global variable pollution.

### Fixed
- **Module Augmentation Typings**: Hardened the "batteries-included" `tempo.index.ts` entry point by explicitly exporting core module types (e.g. `DurationModule`, `FormatModule`). This forces TypeScript to preserve their module augmentations in the compiled `.d.ts` bundle, guaranteeing that methods like `.until()` and `.since()` correctly appear in IDE autocomplete out-of-the-box.

## [3.5.1] - 2026-06-28

### Fixed
Expand Down
13 changes: 13 additions & 0 deletions packages/tempo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,20 @@ For standard usage natively in the browser, use the pre-optimized **Global ESM B
---

## ✨ Why Tempo?

While the native Temporal API gives you perfect primitives (`ZonedDateTime`, `PlainDate`), it doesn't give you business logic. Tempo bridges that gap.

| Feature | Native `Temporal` API | The `Tempo` Ecosystem |
| :--- | :--- | :--- |
| **Primitives** | Perfect (`Instant`, `ZonedDateTime`) | Powered by Native Primitives ✨ |
| **Timezones** | IANA String Support | Advanced fallback & auto-syncing |
| **Domain Logic** | ❌ Build it yourself | ✅ Plugins (`astro`, `sync`) |
| **Natural Language**| ❌ Manual parsing | ✅ "next Friday 3pm" |

### The Missing Domain Layer

* **🏗️ Future Standard**: Built natively on the TC39 `Temporal` proposal. Inherit the reliability of the future standard.
* **🧩 Premium Ecosystem**: Don't build temporal math from scratch. Drop in our cryptographically-secured plugins for Astronomical seasons (`astro`) and atomic state syncing (`sync`).
* **🌍 Zero-Bundle Localization**: Best-in-class multi-language parsing and formatting powered natively by the `Intl` API—no massive static locale dictionaries required.
* **🗣️ Natural Language**: Resolve complex terms like "two days ago" with zero configuration.
* **🧠 Functional Aliases**: Extend the parser with custom logic using a powerful resolution context for relative date math.
Expand Down
17 changes: 17 additions & 0 deletions packages/tempo/doc/ecosystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
![Tempo Plugin](/plugin-logo.svg)

# Plugin Ecosystem

Tempo's functionality can be easily extended through community and premium plugins. Because of Tempo's unique core architecture, plugins add exactly 0 bytes to the bundle size of developers who do not use them.

Browse the live catalog below to discover extensions for scheduling, calendar math, astronomical events, and more.

<CatalogList />

---

::: tip Submit Your Own Plugin!
Have you built something amazing for Tempo? We want to see it!

You can have your own packages listed in this live catalog. Simply build your extension using `defineExtension()` or `defineTerm()` and publish it to NPM with the `"tempo-plugin"` keyword. Open an Issue on our GitHub repository with a link to your package, and we'll review it for inclusion in the global registry!
:::
22 changes: 16 additions & 6 deletions packages/tempo/doc/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,29 @@ To use **Tempo Premium Plugins** via a static CDN, you simply need to explicitly

---

## 📦 Browser (UMD / Global Variable)
## 📦 Browser (Global Variable / Plugins)

If you aren't using ESM or just want a simple `<script>` tag for rapid prototyping, use the UMD global bundle. This attaches `Tempo` to the `window` object.
If you aren't using ESM or just want a simple `<script>` tag for rapid prototyping, use our unified `Magma` global bundles. This clean approach ensures all plugins share a single, collision-free namespace on the `window` object.

```html
<!-- Load the Temporal Polyfill first -->
<!-- 1. Load the Temporal Polyfill first -->
<script src="https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5.1/dist/index.umd.js"></script>

<!-- Load the Tempo Global Bundle -->
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/dist/tempo.bundle.js"></script>
<!-- 2. Load the Tempo Global Bundle (Creates window.Magma) -->
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/dist/tempo.bundle.min.js"></script>

<!-- 3. Load any Community Plugins (Attaches to window.Magma.plugins) -->
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo-plugin-astro@2/dist/index.global.min.js"></script>

<script>
const t = new Tempo('now');
// 1. Extract what you need from the Magma namespace
const { Tempo, plugins } = Magma;

// 2. Extend the core engine
Tempo.extend(plugins.astro);

// 3. Create your instance!
const t = new Tempo('next friday');
console.log(t.toString());
</script>
```
Expand Down
Loading
Loading