diff --git a/src/app/globals.css b/src/app/globals.css index 72cc9f8..f65fe56 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -170,6 +170,18 @@ 0 12px 28px -16px rgba(0, 0, 0, 0.8) ); + /* + * Observatory hero. The panel is deliberately single-look: navy in both + * themes, so these are constants rather than light-dark() pairs. Values + * borrow the dark theme's palette so the hero and dark mode agree. + */ + --hero-bg: #071b2e; + --hero-accent: #8fa8ff; + --hero-cta: #4d67e8; + --hero-trust: #65d2b4; + --hero-amended: #efb65e; + --hero-new: #6693ff; + /* Motion */ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); diff --git a/src/app/page.tsx b/src/app/page.tsx index 853b44e..f69a8b3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -28,12 +28,25 @@ export default async function HomePage() { development.verification.status === 'verified', ) .slice(0, 6); + // Anchored to the data's own currency rather than the wall clock, so the + // figure is deterministic for a given collection state (and satisfies the + // render-purity rule). + const dataCurrentAt = meta.lastHealthyAt ?? meta.lastCollectedAt; + const weekAgo = dataCurrentAt + ? new Date(dataCurrentAt).getTime() - 7 * 24 * 60 * 60 * 1000 + : Number.POSITIVE_INFINITY; + const weeklyDevelopmentCount = allDevelopments.filter( + (development) => + development.status !== 'dismissed' && + new Date(development.detectedAt).getTime() >= weekAgo, + ).length; return ( development.status !== 'dismissed').length} + weeklyDevelopmentCount={weeklyDevelopmentCount} lastCollectedAt={meta.lastCollectedAt} lastHealthyAt={meta.lastHealthyAt} lastReviewedAt={meta.lastReviewedAt} diff --git a/src/components/policy-browser.tsx b/src/components/policy-browser.tsx index 8fa997b..5218458 100644 --- a/src/components/policy-browser.tsx +++ b/src/components/policy-browser.tsx @@ -53,6 +53,8 @@ interface PolicyBrowserProps { policies: Policy[]; developments: Development[]; developmentCount: number; + /** Developments first detected in the last seven days, dismissed excluded. */ + weeklyDevelopmentCount: number; lastCollectedAt: string | null; lastHealthyAt: string | null; lastReviewedAt: string | null; @@ -138,6 +140,7 @@ export function PolicyBrowser({ policies, developments, developmentCount, + weeklyDevelopmentCount, lastCollectedAt, lastHealthyAt, lastReviewedAt, @@ -260,18 +263,18 @@ export function PolicyBrowser({ return (
-
+
-

+

The policy observatory

See Australian AI policy as it changes.

- Policai tracks official AI policy across Australia in real time and - links every record to its source. + Policai tracks official AI policy across Australia, checked daily, + with every record linked to its source.

Search the Australian AI policy register -
+
- + View developments
+ {/* Each figure is a doorway, not a decoration. */}
- {[ - [policies.length, 'verified policies'], - [distinctJurisdictions.size, 'jurisdictions'], - [developmentCount, 'developments'], - [automaticSourceCount, 'sources monitored'], - ].map(([value, label], index) => ( -
0 && 'border-l border-white/20 pl-5')}> - - {value} - - - {label} - -
- ))} + {( + [ + { value: policies.length, label: 'verified policies', href: '#policy-register', delta: null }, + { value: distinctJurisdictions.size, label: 'jurisdictions', href: '/map', delta: null }, + { value: developmentCount, label: 'developments', href: '/developments', delta: weeklyDevelopmentCount }, + { value: automaticSourceCount, label: 'sources monitored', href: '/methodology', delta: null }, + ] as const + ).map((stat, index) => { + const StatLink = stat.href.startsWith('#') ? 'a' : Link; + return ( + 0 && 'border-l border-white/20 pl-5', + )} + > + + {stat.value} + + + {stat.label} + + {stat.delta ? ( + + +{stat.delta} this week + + ) : null} + + ); + })}
@@ -337,13 +358,13 @@ export function PolicyBrowser({
-
+
-

+

What changed recently

- + View all developments @@ -367,7 +388,7 @@ export function PolicyBrowser({ href={development.url} target="_blank" rel="noopener noreferrer" - className="group mt-3 inline-flex max-w-[23rem] items-start gap-2 text-[15px] font-semibold leading-6 hover:text-[#9bb6ff]" + className="group mt-3 inline-flex max-w-[23rem] items-start gap-2 text-[15px] font-semibold leading-6 hover:text-[var(--hero-accent)]" > {development.title} @@ -378,7 +399,7 @@ export function PolicyBrowser({

) : null}

- + {getJurisdictionName(development.jurisdiction)} | Verified source diff --git a/src/components/policy-observatory.tsx b/src/components/policy-observatory.tsx index b3c10f3..8e7c2e0 100644 --- a/src/components/policy-observatory.tsx +++ b/src/components/policy-observatory.tsx @@ -20,9 +20,9 @@ const jurisdictionLabel: Record = { }; const legendItems = [ - { label: 'Active', className: 'bg-[#69c5a3]' }, - { label: 'Amended', className: 'bg-[#efb65e]' }, - { label: 'New (last 30 days)', className: 'bg-[#6693ff]' }, + { label: 'Active', className: 'bg-[var(--hero-trust)]' }, + { label: 'Amended', className: 'bg-[var(--hero-amended)]' }, + { label: 'New (last 30 days)', className: 'bg-[var(--hero-new)]' }, { label: 'Planned / Draft', className: 'border border-white/65 bg-transparent' }, { label: 'Inactive', className: 'bg-white/28' }, ]; @@ -43,11 +43,11 @@ function formatAxisDate(date: Date): string { function pointClass(policy: Policy, date: Date, currentDate: Date): string { const thirtyDays = 30 * 24 * 60 * 60 * 1000; if (currentDate.getTime() - date.getTime() <= thirtyDays && date <= currentDate) { - return 'bg-[#6693ff]'; + return 'bg-[var(--hero-new)]'; } - if (policy.status === 'proposed') return 'border border-white/65 bg-[#071b2e]'; - if (policy.status === 'amended') return 'bg-[#efb65e]'; - if (policy.status === 'active') return 'bg-[#69c5a3]'; + if (policy.status === 'proposed') return 'border border-white/65 bg-[var(--hero-bg)]'; + if (policy.status === 'amended') return 'bg-[var(--hero-amended)]'; + if (policy.status === 'active') return 'bg-[var(--hero-trust)]'; return 'bg-white/28'; } @@ -118,7 +118,7 @@ export function PolicyObservatory({ key={jurisdiction} className="grid h-[3.15rem] grid-cols-[9.5rem_1fr] items-center" > - + {jurisdictionLabel[jurisdiction]} {jurisdictionPolicies.length} @@ -135,7 +135,7 @@ export function PolicyObservatory({ title={`${policy.title} — ${formatAxisDate(date)}`} aria-label={`Open ${policy.title}`} className={cn( - 'absolute z-20 block h-[9px] w-[9px] -translate-x-1/2 -translate-y-1/2 rounded-full ring-1 ring-[#071b2e] transition duration-150 hover:z-30 hover:scale-[1.75] focus:z-30 focus:scale-[1.75]', + 'absolute z-20 block h-[9px] w-[9px] -translate-x-1/2 -translate-y-1/2 rounded-full ring-1 ring-[var(--hero-bg)] transition duration-150 hover:z-30 hover:scale-[1.75] focus:z-30 focus:scale-[1.75]', pointClass(policy, date, currentDate), )} style={{