@circadian/sol 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,607 @@
1
+ <div align="center">
2
+
3
+ <img src=".github/banner.png" alt="@circadian/sol — solar-aware React widgets" width="100%" />
4
+
5
+ <br />
6
+ <br />
7
+
8
+ <a href="https://www.npmjs.com/package/@circadian/sol">
9
+ <img src="https://img.shields.io/npm/v/@circadian/sol?style=flat-square&color=111&labelColor=111&logo=npm" alt="npm version" />
10
+ </a>
11
+ <a href="https://www.npmjs.com/package/@circadian/sol">
12
+ <img src="https://img.shields.io/npm/dm/@circadian/sol?style=flat-square&color=111&labelColor=111" alt="npm downloads" />
13
+ </a>
14
+ <a href="https://github.com/circadian-dev/sol/blob/main/LICENSE">
15
+ <img src="https://img.shields.io/npm/l/@circadian/sol?style=flat-square&color=111&labelColor=111" alt="license" />
16
+ </a>
17
+ <a href="https://github.com/circadian-dev/sol/actions/workflows/validate.yml">
18
+ <img src="https://img.shields.io/github/actions/workflow/status/circadian-dev/sol/validate.yml?style=flat-square&color=111&labelColor=111&label=ci" alt="CI" />
19
+ </a>
20
+
21
+ <br />
22
+ <br />
23
+
24
+ **Solar-aware React widgets that follow the real position of the sun.**
25
+
26
+ [npm](https://www.npmjs.com/package/@circadian/sol) · [GitHub](https://github.com/circadian-dev/sol) · [circadian.dev](https://circadian.dev)
27
+
28
+ </div>
29
+
30
+ ---
31
+
32
+ > **Dark mode reacts to a preference. Sol reacts to place and time.**
33
+
34
+ Most apps treat theming as a binary choice — light or dark, on or off, a toggle buried in settings.
35
+
36
+ Sol replaces that with something alive. It computes the sun's real position from the user's location, timezone, and current time, then smoothly transitions the interface through **9 solar phases** — dawn, sunrise, morning, solar noon, afternoon, sunset, dusk, night, and midnight — with animated blends, optional weather layers, and 10 richly designed skins.
37
+
38
+ No API key. No manual toggle. Your UI just follows the sun.
39
+
40
+ Sol is the flagship package from [Circadian](https://circadian.dev) — a platform for ambient-aware UI.
41
+
42
+ ---
43
+
44
+ ```bash
45
+ bun add @circadian/sol
46
+ # or
47
+ npm install @circadian/sol
48
+ ```
49
+
50
+ `@circadian/sol` gives you a full `SolarWidget`, a `CompactWidget`, 10 skins, 9 solar phases, optional live weather, optional flag display, and a dev-only timeline scrubber via `SolarDevTools`. Solar position is computed locally from latitude, longitude, timezone, and current time — no solar API required.
51
+
52
+ ---
53
+
54
+ ## Features
55
+
56
+ - **2 widget variants** — `SolarWidget` (full card) and `CompactWidget` (slim pill/bar)
57
+ - **10 skins** — `foundry`, `paper`, `signal`, `meridian`, `mineral`, `aurora`, `tide`, `void`, `sundial`, `parchment`
58
+ - **9 solar phases** — `midnight`, `night`, `dawn`, `sunrise`, `morning`, `solar-noon`, `afternoon`, `sunset`, `dusk`
59
+ - **Built-in fallback strategy** — geolocation → browser timezone → timezone centroid
60
+ - **Optional live weather** — powered by Open-Meteo (no API key required)
61
+ - **Dev preview tooling** — `SolarDevTools` lets you scrub through the day and preview phase colors
62
+ - **SSR-safe** — works in Next.js, Remix, TanStack Start, Blade, and Vite
63
+
64
+ ---
65
+
66
+ ## Quick Start
67
+
68
+ Sol uses browser APIs for geolocation and solar computation. The exact setup depends on your framework — pick yours below.
69
+
70
+ ---
71
+
72
+ ### Vite
73
+
74
+ No special setup needed. Wrap your app with the provider and use widgets directly.
75
+
76
+ ```tsx
77
+ // main.tsx
78
+ import { StrictMode } from 'react';
79
+ import { createRoot } from 'react-dom/client';
80
+ import { SolarThemeProvider } from '@circadian/sol';
81
+ import App from './App';
82
+
83
+ createRoot(document.getElementById('root')!).render(
84
+ <StrictMode>
85
+ <SolarThemeProvider initialDesign="foundry">
86
+ <App />
87
+ </SolarThemeProvider>
88
+ </StrictMode>,
89
+ );
90
+ ```
91
+
92
+ ```tsx
93
+ // App.tsx
94
+ import { SolarWidget } from '@circadian/sol';
95
+
96
+ export default function App() {
97
+ return <SolarWidget showWeather showFlag />;
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ### Next.js (App Router)
104
+
105
+ Add `'use client'` at the top of any file that uses Sol. This marks it as a client component and prevents it from running during server rendering.
106
+
107
+ ```tsx
108
+ // components/providers.tsx
109
+ 'use client';
110
+ import { SolarThemeProvider } from '@circadian/sol';
111
+
112
+ export default function Providers({ children }: { children: React.ReactNode }) {
113
+ return (
114
+ <SolarThemeProvider initialDesign="foundry">
115
+ {children}
116
+ </SolarThemeProvider>
117
+ );
118
+ }
119
+ ```
120
+
121
+ ```tsx
122
+ // components/solar-widget.tsx
123
+ 'use client';
124
+ import { SolarWidget } from '@circadian/sol';
125
+
126
+ export default function Solar() {
127
+ return <SolarWidget showWeather showFlag />;
128
+ }
129
+ ```
130
+
131
+ ```tsx
132
+ // app/layout.tsx
133
+ import Providers from '../components/providers';
134
+
135
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
136
+ return (
137
+ <html lang="en">
138
+ <body>
139
+ <Providers>{children}</Providers>
140
+ </body>
141
+ </html>
142
+ );
143
+ }
144
+ ```
145
+
146
+ ```tsx
147
+ // app/page.tsx
148
+ import Solar from '../components/solar-widget';
149
+
150
+ export default function Page() {
151
+ return <Solar />;
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ### Remix
158
+
159
+ Name any file that uses Sol with a `.client.tsx` extension. Remix excludes `.client` files from the server bundle automatically.
160
+
161
+ ```tsx
162
+ // app/components/providers.client.tsx
163
+ import { SolarThemeProvider } from '@circadian/sol';
164
+
165
+ export default function Providers({ children }: { children: React.ReactNode }) {
166
+ return (
167
+ <SolarThemeProvider initialDesign="foundry">
168
+ {children}
169
+ </SolarThemeProvider>
170
+ );
171
+ }
172
+ ```
173
+
174
+ ```tsx
175
+ // app/components/solar-widget.client.tsx
176
+ import { SolarWidget } from '@circadian/sol';
177
+
178
+ export default function Solar() {
179
+ return <SolarWidget showWeather showFlag />;
180
+ }
181
+ ```
182
+
183
+ ```tsx
184
+ // app/root.tsx
185
+ import Providers from './components/providers.client';
186
+
187
+ export default function App() {
188
+ return (
189
+ <html lang="en">
190
+ <body>
191
+ <Providers>
192
+ <Outlet />
193
+ </Providers>
194
+ </body>
195
+ </html>
196
+ );
197
+ }
198
+ ```
199
+
200
+ ```tsx
201
+ // app/routes/_index.tsx
202
+ import Solar from '../components/solar-widget.client';
203
+
204
+ export default function Index() {
205
+ return <Solar />;
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ### TanStack Start
212
+
213
+ Use the `ClientOnly` component from `@tanstack/react-router` to prevent Sol from rendering during SSR.
214
+
215
+ ```tsx
216
+ // app/components/solar-widget.tsx
217
+ import { ClientOnly } from '@tanstack/react-router';
218
+ import { SolarThemeProvider, SolarWidget } from '@circadian/sol';
219
+
220
+ export default function Solar() {
221
+ return (
222
+ <ClientOnly fallback={null}>
223
+ <SolarThemeProvider initialDesign="foundry">
224
+ <SolarWidget showWeather showFlag />
225
+ </SolarThemeProvider>
226
+ </ClientOnly>
227
+ );
228
+ }
229
+ ```
230
+
231
+ ```tsx
232
+ // app/routes/index.tsx
233
+ import Solar from '../components/solar-widget';
234
+
235
+ export const Route = createFileRoute('/')({
236
+ component: () => <Solar />,
237
+ });
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Blade
243
+
244
+ Name any file that uses Sol with a `.client.tsx` extension. Blade runs pages server-side; component files run client-side.
245
+
246
+ ```tsx
247
+ // components/providers.client.tsx
248
+ import { SolarThemeProvider } from '@circadian/sol';
249
+
250
+ export default function Providers({ children }: { children: React.ReactNode }) {
251
+ return (
252
+ <SolarThemeProvider initialDesign="foundry">
253
+ {children}
254
+ </SolarThemeProvider>
255
+ );
256
+ }
257
+ ```
258
+
259
+ ```tsx
260
+ // components/solar-widget.client.tsx
261
+ import { SolarWidget } from '@circadian/sol';
262
+
263
+ export default function Solar() {
264
+ return <SolarWidget showWeather showFlag />;
265
+ }
266
+ ```
267
+
268
+ ```tsx
269
+ // pages/layout.tsx
270
+ import Providers from '../components/providers.client';
271
+
272
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
273
+ return <Providers>{children}</Providers>;
274
+ }
275
+ ```
276
+
277
+ ```tsx
278
+ // pages/index.tsx
279
+ import Solar from '../components/solar-widget.client';
280
+
281
+ export default function Page() {
282
+ return <Solar />;
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Provider Props
289
+
290
+ `SolarThemeProvider` is the shared runtime for solar phase computation, timezone, coordinates, and skin selection.
291
+
292
+ | Prop | Type | Default | Description |
293
+ |---|---|---|---|
294
+ | `children` | `ReactNode` | — | Required |
295
+ | `initialDesign` | `DesignMode` | `'foundry'` | Starting skin |
296
+ | `isolated` | `boolean` | `false` | Scope CSS vars to wrapper div instead of `:root`. Useful when mounting multiple providers on a single page. |
297
+
298
+ ### Location is automatic
299
+
300
+ `SolarThemeProvider` resolves the user's location using a 3-step fallback:
301
+
302
+ 1. **Browser Geolocation API** — most accurate, requires user permission
303
+ 2. **Browser timezone** (`Intl.DateTimeFormat`) — instant, no permission needed
304
+ 3. **Timezone centroid lookup** — maps the IANA timezone to approximate coordinates
305
+
306
+ Solar phases are accurate to ~15–30 minutes from timezone alone, and refine to exact values when geolocation is granted.
307
+
308
+ ---
309
+
310
+ ## SolarWidget
311
+
312
+ The full card widget. Reads its design from the nearest `SolarThemeProvider`.
313
+
314
+ ```tsx
315
+ <SolarWidget
316
+ expandDirection="top-left"
317
+ size="lg"
318
+ showWeather
319
+ showFlag
320
+ hoverEffect
321
+ />
322
+ ```
323
+
324
+ ### Props
325
+
326
+ | Prop | Type | Default | Description |
327
+ |---|---|---|---|
328
+ | `expandDirection` | `ExpandDirection` | `'bottom-right'` | Direction the card expands |
329
+ | `size` | `WidgetSize` | `'lg'` | Widget size |
330
+ | `showWeather` | `boolean` | `false` | Enable live weather display |
331
+ | `showFlag` | `boolean` | `false` | Show country flag |
332
+ | `hoverEffect` | `boolean` | `false` | Enable hover animation |
333
+ | `phaseOverride` | `SolarPhase` | — | Force a discrete phase |
334
+ | `simulatedDate` | `Date` | — | Simulate a specific time |
335
+ | `weatherCategoryOverride` | `WeatherCategory \| null` | — | Force weather condition |
336
+ | `customPalettes` | `CustomPalettes` | — | Override phase colors per phase |
337
+ | `forceExpanded` | `boolean` | — | Lock expanded or collapsed state |
338
+
339
+ ---
340
+
341
+ ## CompactWidget
342
+
343
+ <div align="center">
344
+ <img src=".github/compact-banner.png" alt="CompactWidget skins - Tide at Drift, Paper at Morning, Meridian at Dawn" width="100%"/>
345
+ </div>
346
+
347
+ The slim pill/bar variant. Accepts an optional `design` prop to override the provider's active skin.
348
+
349
+ ```tsx
350
+ <CompactWidget
351
+ design="signal"
352
+ size="md"
353
+ showWeather
354
+ showFlag
355
+ showTemperature
356
+ />
357
+ ```
358
+
359
+ ### Props
360
+
361
+ | Prop | Type | Default | Description |
362
+ |---|---|---|---|
363
+ | `design` | `DesignMode` | provider design | Design/skin override for this widget |
364
+ | `size` | `CompactSize` | `'md'` | Compact size |
365
+ | `showWeather` | `boolean` | `false` | Show weather icon |
366
+ | `showFlag` | `boolean` | `false` | Show country flag |
367
+ | `showTemperature` | `boolean` | `true` | Show live temperature |
368
+ | `overridePhase` | `SolarPhase \| null` | — | Force a discrete phase |
369
+ | `customPalettes` | `CustomPalettes` | — | Override bg gradient per phase |
370
+ | `simulatedDate` | `Date` | — | Simulate a time |
371
+ | `className` | `string` | — | Wrapper CSS class |
372
+
373
+ ---
374
+
375
+ ## Skins
376
+
377
+ 10 designs, each with a full widget and compact variant. If `design` is omitted on `CompactWidget`, it uses the provider's active design. `SolarWidget` always uses the provider's active design.
378
+
379
+ ```ts
380
+ type DesignMode =
381
+ | 'aurora' // luminous ethereal
382
+ | 'foundry' // warm volumetric industrial
383
+ | 'tide' // fluid organic wave
384
+ | 'void' // minimal negative space
385
+ | 'mineral' // faceted crystal gem
386
+ | 'meridian' // hairline geometric
387
+ | 'signal' // pixel/blocky lo-fi
388
+ | 'paper' // flat ink editorial
389
+ | 'sundial' // roman/classical carved
390
+ | 'parchment'; // document strokes
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Positioning
396
+
397
+ ```tsx
398
+ <SolarWidget /> // inline (default)
399
+ <SolarWidget position="bottom-right" /> // fixed to viewport
400
+ <SolarWidget position="bottom-right" expandDirection="top-left" /> // with expand direction
401
+ ```
402
+
403
+ Supported positions: `top-left` `top-center` `top-right` `center-left` `center` `center-right` `bottom-left` `bottom-center` `bottom-right` `inline`
404
+
405
+ ---
406
+
407
+ ## Weather
408
+
409
+ ```tsx
410
+ <SolarWidget showWeather />
411
+
412
+ // Force a category for preview
413
+ <SolarWidget showWeather weatherCategoryOverride="thunder" />
414
+ ```
415
+
416
+ Powered by [Open-Meteo](https://open-meteo.com/) — free, no API key. Available categories: `clear` `partly-cloudy` `overcast` `fog` `drizzle` `rain` `heavy-rain` `snow` `heavy-snow` `thunder`
417
+
418
+ ---
419
+
420
+ ## Phase & Time Overrides
421
+
422
+ ```tsx
423
+ // Force a discrete phase
424
+ <SolarWidget phaseOverride="sunset" />
425
+
426
+ // Simulate a specific time (with blend)
427
+ const preview = new Date();
428
+ preview.setHours(6, 45, 0, 0);
429
+ <SolarWidget simulatedDate={preview} />
430
+ ```
431
+
432
+ Use `simulatedDate` for realistic continuous previews. Use `phaseOverride` for simple hard overrides.
433
+
434
+ ---
435
+
436
+ ## Custom Palettes
437
+
438
+ Override the background gradient for any phase on any skin. Works on both `SolarWidget` and `CompactWidget`.
439
+
440
+ ```tsx
441
+ <SolarWidget
442
+ customPalettes={{
443
+ dawn: { bg: ['#20122a', '#7f3b5d', '#f5a66e'] },
444
+ sunset: { bg: ['#2e0f18', '#b84a3d', '#ffbe7a'] },
445
+ }}
446
+ />
447
+
448
+ <CompactWidget
449
+ customPalettes={{
450
+ dawn: { bg: ['#20122a', '#7f3b5d', '#f5a66e'] },
451
+ sunset: { bg: ['#2e0f18', '#b84a3d', '#ffbe7a'] },
452
+ }}
453
+ />
454
+ ```
455
+
456
+ Each `bg` is a 3-stop gradient: `[top, middle, bottom]`. Only the phases you specify are overridden — the rest keep the skin's default colors. All skin-specific elements (orbs, glows, text, tracks) remain unchanged; only the background gradient is replaced.
457
+
458
+ ---
459
+
460
+ ## SolarDevTools
461
+
462
+ When your interface depends on live solar time, manual testing breaks down fast — you can't wait until sunset to test sunset. `SolarDevTools` lets you scrub through the full day in seconds, preview every one of the **9 phases**, test every skin against every time of day, and catch phase-specific visual bugs before your users do.
463
+
464
+ Imported from a dedicated subpath — never included in production bundles unless explicitly imported.
465
+
466
+ ```tsx
467
+ import { SolarDevTools } from '@circadian/sol/devtools';
468
+
469
+ // Vite
470
+ {import.meta.env.DEV && <SolarDevTools />}
471
+
472
+ // Next.js / Remix / TanStack Start / Blade
473
+ {process.env.NODE_ENV === 'development' && <SolarDevTools />}
474
+ ```
475
+
476
+ ### Full example
477
+
478
+ ```tsx
479
+ import { SolarThemeProvider, SolarWidget } from '@circadian/sol';
480
+ import { SolarDevTools } from '@circadian/sol/devtools';
481
+
482
+ export default function Demo() {
483
+ return (
484
+ <SolarThemeProvider initialDesign="foundry">
485
+ <SolarWidget showWeather showFlag />
486
+ {process.env.NODE_ENV === 'development' && (
487
+ <SolarDevTools position="bottom-center" />
488
+ )}
489
+ </SolarThemeProvider>
490
+ );
491
+ }
492
+ ```
493
+
494
+ ### Props
495
+
496
+ | Prop | Type | Default | Description |
497
+ |---|---|---|---|
498
+ | `defaultOpen` | `boolean` | `false` | Start expanded |
499
+ | `position` | `'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'bottom-center'` | Pill position |
500
+ | `enabled` | `boolean` | `true` | Programmatic enable/disable |
501
+
502
+ ---
503
+
504
+ ## useSolarTheme
505
+
506
+ ```tsx
507
+ import { useSolarTheme } from '@circadian/sol';
508
+
509
+ function DebugPanel() {
510
+ const { phase, timezone, latitude, longitude, design } = useSolarTheme();
511
+ return (
512
+ <pre>{JSON.stringify({ phase, timezone, latitude, longitude, design }, null, 2)}</pre>
513
+ );
514
+ }
515
+ ```
516
+
517
+ ### Return shape
518
+
519
+ | Property | Type | Description |
520
+ |---|---|---|
521
+ | `phase` | `SolarPhase` | Current active phase |
522
+ | `blend` | `SolarBlend` | Phase blend state (phase, nextPhase, t) |
523
+ | `isDaytime` | `boolean` | Whether the sun is above the horizon |
524
+ | `brightness` | `number` | 0–1 brightness value |
525
+ | `mode` | `'light' \| 'dim' \| 'dark'` | Current light mode |
526
+ | `accentColor` | `string` | Active accent hex |
527
+ | `timezone` | `string \| null` | Resolved timezone |
528
+ | `latitude` | `number \| null` | Resolved latitude |
529
+ | `longitude` | `number \| null` | Resolved longitude |
530
+ | `coordsReady` | `boolean` | Whether coordinates have resolved |
531
+ | `design` | `DesignMode` | Active skin name |
532
+ | `activeSkin` | `SkinDefinition` | Full skin definition object |
533
+ | `setOverridePhase` | `(phase \| null) => void` | Set/clear phase override |
534
+ | `setDesign` | `(skin: DesignMode) => void` | Change active skin |
535
+
536
+ ---
537
+
538
+ ## Multiple Widgets
539
+
540
+ ```tsx
541
+ <SolarThemeProvider initialDesign="foundry">
542
+ <SolarWidget showWeather />
543
+ <CompactWidget design="signal" />
544
+ <SolarWidget />
545
+ </SolarThemeProvider>
546
+ ```
547
+
548
+ `CompactWidget` accepts a `design` prop to override per-instance. `SolarWidget` always follows the provider. The provider manages shared solar state — location, phase, and weather are computed once and shared across all children.
549
+
550
+ ---
551
+
552
+ ## TypeScript
553
+
554
+ ```ts
555
+ import type {
556
+ DesignMode,
557
+ SolarPhase,
558
+ SolarBlend,
559
+ WeatherCategory,
560
+ ExpandDirection,
561
+ WidgetSize,
562
+ CompactSize,
563
+ SkinDefinition,
564
+ WidgetPalette,
565
+ CustomPalettes,
566
+ SolarTheme,
567
+ } from '@circadian/sol';
568
+ ```
569
+
570
+ ---
571
+
572
+ ## What's Included
573
+
574
+ | | |
575
+ |---|---|
576
+ | ✅ | Full widget + compact widget |
577
+ | ✅ | 10 skins with full + compact variants |
578
+ | ✅ | Solar math (NOAA equations, no external API) |
579
+ | ✅ | Timezone fallback logic |
580
+ | ✅ | Optional live weather (Open-Meteo) |
581
+ | ✅ | Skin-aware country flags |
582
+ | ✅ | Dev timeline scrubber |
583
+ | ✅ | Self-contained CSS (no Tailwind required in host app) |
584
+ | ✅ | SSR-safe (Next.js, Remix, TanStack Start, Blade, Vite) |
585
+ | ❌ | No solar API key needed |
586
+ | ❌ | No weather API key needed |
587
+ | ❌ | No Tailwind needed in your app |
588
+ | ❌ | No geolocation permission required |
589
+
590
+ ---
591
+
592
+ ## Coming Soon
593
+
594
+ Sol is actively being developed. Things in progress:
595
+
596
+ - **Seasonal theme system** — 4 seasons (Summer, Autumn, Winter, Spring) that blend automatically with the existing 9-phase system, computed from date and location with no configuration required
597
+ - More skins
598
+ - Vue and Svelte adapters
599
+ - Deep token override system
600
+
601
+ ---
602
+
603
+ <div align="center">
604
+
605
+ MIT © [Circadian] - website coming soon
606
+
607
+ </div>
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+
3
+ //#region src/devtools/solar-devtools.d.ts
4
+ interface SolarDevToolsProps {
5
+ defaultOpen?: boolean;
6
+ position?: 'bottom-left' | 'bottom-center' | 'bottom-right';
7
+ enabled?: boolean;
8
+ }
9
+ declare function SolarDevTools({
10
+ defaultOpen,
11
+ position,
12
+ enabled
13
+ }: SolarDevToolsProps): React.ReactPortal | null;
14
+ //#endregion
15
+ export { SolarDevTools, type SolarDevToolsProps };
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/devtools/solar-devtools.tsx"],"sourcesContent":[],"mappings":";;;UA6BiB,kBAAA;;EAAA,QAAA,CAAA,EAAA,aAAkB,GAAA,eAAA,GAAA,cAAA;EAqCnB,OAAA,CAAA,EAAA,OAAa;;AAE3B,iBAFc,aAAA,CAEd;EAAA,WAAA;EAAA,QAAA;EAAA;AAAA,CAAA,EAEC,kBAFD,CAAA,EAEmB,KAAA,CAAA,WAFnB,GAAA,IAAA"}