@drakkar.software/octospaces-ui 0.1.0
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/dist/index.d.ts +209 -0
- package/dist/index.js +107 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/index.ts +39 -0
- package/src/theme/helpers.test.ts +168 -0
- package/src/theme/helpers.ts +115 -0
- package/src/theme/provider.tsx +54 -0
- package/src/theme/types.ts +167 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +12 -0
- package/vitest.config.ts +9 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Theme contract for `@drakkar.software/octospaces-ui`.
|
|
5
|
+
*
|
|
6
|
+
* The package ships NO theme VALUES — only these type definitions. The host app
|
|
7
|
+
* constructs a concrete {@link Theme} object (with its own palette, tokens, and
|
|
8
|
+
* scheme logic) and passes it to `<OctoSpacesThemeProvider theme={…}>` at the root.
|
|
9
|
+
*
|
|
10
|
+
* The {@link Palette} interface is the SUPERSET of OctoChat + OctoVault so the UI
|
|
11
|
+
* primitives work correctly in both apps. Apps that don't use vault-specific keys
|
|
12
|
+
* (like `editorCanvas`) can safely set them to a sensible default.
|
|
13
|
+
*/
|
|
14
|
+
/** The active color scheme. Passed as part of `Theme.scheme`. */
|
|
15
|
+
type ColorScheme = 'light' | 'dark';
|
|
16
|
+
/**
|
|
17
|
+
* Full color palette contract. OctoVault's palette is the superset. OctoChat apps
|
|
18
|
+
* add `editorCanvas`, `tooltipBg`, `onTooltip` as they migrate to the shared UI.
|
|
19
|
+
*
|
|
20
|
+
* All colors are CSS/RN color strings (hex, rgba, or named color tokens).
|
|
21
|
+
*/
|
|
22
|
+
interface Palette {
|
|
23
|
+
background: string;
|
|
24
|
+
surface: string;
|
|
25
|
+
surfaceElevated: string;
|
|
26
|
+
surfaceModal: string;
|
|
27
|
+
surfaceInput: string;
|
|
28
|
+
sidebar: string;
|
|
29
|
+
sidebarActive: string;
|
|
30
|
+
border: string;
|
|
31
|
+
borderSubtle: string;
|
|
32
|
+
borderStrong: string;
|
|
33
|
+
text: string;
|
|
34
|
+
textSecondary: string;
|
|
35
|
+
textTertiary: string;
|
|
36
|
+
textDisabled: string;
|
|
37
|
+
textInverse: string;
|
|
38
|
+
textOnPrimary: string;
|
|
39
|
+
primary: string;
|
|
40
|
+
primaryHover: string;
|
|
41
|
+
primaryMuted: string;
|
|
42
|
+
primarySubtle: string;
|
|
43
|
+
success: string;
|
|
44
|
+
successMuted: string;
|
|
45
|
+
warning: string;
|
|
46
|
+
warningMuted: string;
|
|
47
|
+
danger: string;
|
|
48
|
+
dangerMuted: string;
|
|
49
|
+
info: string;
|
|
50
|
+
infoMuted: string;
|
|
51
|
+
presenceOnline: string;
|
|
52
|
+
presenceAway: string;
|
|
53
|
+
presenceBusy: string;
|
|
54
|
+
presenceOffline: string;
|
|
55
|
+
verificationVerified: string;
|
|
56
|
+
verificationPartial: string;
|
|
57
|
+
verificationNone: string;
|
|
58
|
+
overlay: string;
|
|
59
|
+
shadow: string;
|
|
60
|
+
focus: string;
|
|
61
|
+
skeleton: string;
|
|
62
|
+
skeletonShimmer: string;
|
|
63
|
+
editorCanvas: string;
|
|
64
|
+
tooltipBg: string;
|
|
65
|
+
onTooltip: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Static spacing scale. Host app provides an object with numeric pixel values
|
|
69
|
+
* keyed by token name. E.g. `{ '0': 0, '1': 4, '2': 8, '3': 12, '4': 16, … }`.
|
|
70
|
+
*/
|
|
71
|
+
type Spacing = Record<string, number>;
|
|
72
|
+
/**
|
|
73
|
+
* Border-radius scale.
|
|
74
|
+
*/
|
|
75
|
+
type Radii = Record<string, number | string>;
|
|
76
|
+
/**
|
|
77
|
+
* Typography scale: font sizes, line heights, weights — keyed by variant name.
|
|
78
|
+
*/
|
|
79
|
+
interface TypeScale {
|
|
80
|
+
size: number;
|
|
81
|
+
lineHeight: number;
|
|
82
|
+
weight?: string | number;
|
|
83
|
+
letterSpacing?: number;
|
|
84
|
+
}
|
|
85
|
+
type Typography = Record<string, TypeScale>;
|
|
86
|
+
/**
|
|
87
|
+
* Font family config. Keys are semantic names; values are font-family strings.
|
|
88
|
+
*/
|
|
89
|
+
type Fonts = Record<string, string>;
|
|
90
|
+
/** Easing curve definitions (e.g. for Reanimated `withTiming`). */
|
|
91
|
+
type Easing = Record<string, number[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Motion token: duration + easing pairs keyed by animation name.
|
|
94
|
+
*/
|
|
95
|
+
interface MotionToken {
|
|
96
|
+
duration: number;
|
|
97
|
+
easing?: number[];
|
|
98
|
+
}
|
|
99
|
+
type Motion = Record<string, MotionToken>;
|
|
100
|
+
/** Shadow / elevation definitions. */
|
|
101
|
+
interface ShadowToken {
|
|
102
|
+
shadowColor?: string;
|
|
103
|
+
shadowOffset?: {
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
};
|
|
107
|
+
shadowOpacity?: number;
|
|
108
|
+
shadowRadius?: number;
|
|
109
|
+
elevation?: number;
|
|
110
|
+
}
|
|
111
|
+
type Shadows = Record<string, ShadowToken>;
|
|
112
|
+
/** Layout constants (screen margins, nav heights, etc.). */
|
|
113
|
+
type Layout = Record<string, number>;
|
|
114
|
+
/** Opacity scale. */
|
|
115
|
+
type Opacity = Record<string, number>;
|
|
116
|
+
/** Named color swatches (beyond the palette — brand accents, label colors). */
|
|
117
|
+
type Swatches = Record<string, string>;
|
|
118
|
+
/** z-index layers. */
|
|
119
|
+
type Layers = Record<string, number>;
|
|
120
|
+
/** Letter-spacing presets keyed by variant name. */
|
|
121
|
+
type LabelTracking = Record<string, number>;
|
|
122
|
+
/**
|
|
123
|
+
* The complete Theme object the host app constructs and injects.
|
|
124
|
+
* All primitives in this package read ONLY from this injected Theme.
|
|
125
|
+
*/
|
|
126
|
+
interface Theme {
|
|
127
|
+
scheme: ColorScheme;
|
|
128
|
+
colors: Palette;
|
|
129
|
+
spacing: Spacing;
|
|
130
|
+
radii: Radii;
|
|
131
|
+
type: Typography;
|
|
132
|
+
fonts: Fonts;
|
|
133
|
+
motion: Motion;
|
|
134
|
+
shadows: Shadows;
|
|
135
|
+
layout: Layout;
|
|
136
|
+
opacity: Opacity;
|
|
137
|
+
swatches: Swatches;
|
|
138
|
+
layers: Layers;
|
|
139
|
+
easing: Easing;
|
|
140
|
+
labelTracking: LabelTracking;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Theme injection plumbing — provider + hook.
|
|
145
|
+
*
|
|
146
|
+
* The package carries ZERO theme values. The host app builds a concrete {@link Theme}
|
|
147
|
+
* and wraps its tree in `<OctoSpacesThemeProvider theme={resolvedTheme}>`.
|
|
148
|
+
* All primitives then call `useOctoSpacesTheme()` to read the active theme.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
interface OctoSpacesThemeProviderProps {
|
|
152
|
+
theme: Theme;
|
|
153
|
+
children: React.ReactNode;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Wrap your root component with this provider to inject the resolved Theme into
|
|
157
|
+
* every primitive from `@drakkar.software/octospaces-ui`.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```tsx
|
|
161
|
+
* import { OctoSpacesThemeProvider } from '@drakkar.software/octospaces-ui';
|
|
162
|
+
* import { resolvedTheme } from '@/theme'; // your app's theme
|
|
163
|
+
*
|
|
164
|
+
* export default function App() {
|
|
165
|
+
* return (
|
|
166
|
+
* <OctoSpacesThemeProvider theme={resolvedTheme}>
|
|
167
|
+
* <RootNavigator />
|
|
168
|
+
* </OctoSpacesThemeProvider>
|
|
169
|
+
* );
|
|
170
|
+
* }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
declare function OctoSpacesThemeProvider({ theme, children }: OctoSpacesThemeProviderProps): React.JSX.Element;
|
|
174
|
+
/**
|
|
175
|
+
* Read the active theme. Throws if called outside an `<OctoSpacesThemeProvider>` —
|
|
176
|
+
* this is intentional: a missing provider means primitives have no colors/spacing,
|
|
177
|
+
* so a hard failure with a clear message is better than a silent rendering bug.
|
|
178
|
+
*/
|
|
179
|
+
declare function useOctoSpacesTheme(): Theme;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Pure palette-helper functions over a {@link Palette}. No theme values live here —
|
|
183
|
+
* only functions that DERIVE from the injected palette (or from passed-in colors).
|
|
184
|
+
*
|
|
185
|
+
* Import these from `@drakkar.software/octospaces-ui` (re-exported by `src/index.ts`).
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/** Map a presence status string to the corresponding palette color. */
|
|
189
|
+
declare function presenceColor(palette: Palette, status: 'online' | 'away' | 'busy' | 'offline' | string): string;
|
|
190
|
+
/** Map a verification level to the corresponding palette color. */
|
|
191
|
+
declare function verificationColor(palette: Palette, level: 'verified' | 'partial' | 'none' | string): string;
|
|
192
|
+
/** Stable avatar background tint derived from a userId string. */
|
|
193
|
+
declare function avatarTint(palette: Palette, userId: string): string;
|
|
194
|
+
/** Look up a named swatch; falls back to `palette.primary` if absent. */
|
|
195
|
+
declare function swatch(theme: Theme, name: string): string;
|
|
196
|
+
/** Derive a `borderColor` value for a "paper" (elevated surface) border. */
|
|
197
|
+
declare function paperBorder(palette: Palette): string;
|
|
198
|
+
/** Build a glow shadow token from a base color (used for focus rings, highlights). */
|
|
199
|
+
declare function glowShadow(color: string, radius?: number, opacity?: number): ShadowToken;
|
|
200
|
+
/** Style object for a keyboard-focus indicator (web + React Native). */
|
|
201
|
+
declare function focusRingStyle(palette: Palette, width?: number): {
|
|
202
|
+
borderWidth: number;
|
|
203
|
+
borderColor: string;
|
|
204
|
+
borderStyle: 'solid';
|
|
205
|
+
};
|
|
206
|
+
/** Map a semantic status name to its palette color. */
|
|
207
|
+
declare function statusColor(palette: Palette, status: 'success' | 'warning' | 'danger' | 'info' | string, muted?: boolean): string;
|
|
208
|
+
|
|
209
|
+
export { type ColorScheme, type Easing, type Fonts, type LabelTracking, type Layers, type Layout, type Motion, type MotionToken, OctoSpacesThemeProvider, type OctoSpacesThemeProviderProps, type Opacity, type Palette, type Radii, type ShadowToken, type Shadows, type Spacing, type Swatches, type Theme, type TypeScale, type Typography, avatarTint, focusRingStyle, glowShadow, paperBorder, presenceColor, statusColor, swatch, useOctoSpacesTheme, verificationColor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/theme/provider.tsx
|
|
2
|
+
import React, { createContext, useContext } from "react";
|
|
3
|
+
var ThemeContext = createContext(null);
|
|
4
|
+
function OctoSpacesThemeProvider({ theme, children }) {
|
|
5
|
+
return /* @__PURE__ */ React.createElement(ThemeContext.Provider, { value: theme }, children);
|
|
6
|
+
}
|
|
7
|
+
function useOctoSpacesTheme() {
|
|
8
|
+
const theme = useContext(ThemeContext);
|
|
9
|
+
if (!theme) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"[octospaces-ui] useOctoSpacesTheme() called outside of <OctoSpacesThemeProvider>. Wrap your root component with <OctoSpacesThemeProvider theme={\u2026}>."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return theme;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/theme/helpers.ts
|
|
18
|
+
function presenceColor(palette, status) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case "online":
|
|
21
|
+
return palette.presenceOnline;
|
|
22
|
+
case "away":
|
|
23
|
+
return palette.presenceAway;
|
|
24
|
+
case "busy":
|
|
25
|
+
return palette.presenceBusy;
|
|
26
|
+
default:
|
|
27
|
+
return palette.presenceOffline;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function verificationColor(palette, level) {
|
|
31
|
+
switch (level) {
|
|
32
|
+
case "verified":
|
|
33
|
+
return palette.verificationVerified;
|
|
34
|
+
case "partial":
|
|
35
|
+
return palette.verificationPartial;
|
|
36
|
+
default:
|
|
37
|
+
return palette.verificationNone;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var AVATAR_TINT_KEYS = [
|
|
41
|
+
"primary",
|
|
42
|
+
"success",
|
|
43
|
+
"warning",
|
|
44
|
+
"danger",
|
|
45
|
+
"info"
|
|
46
|
+
];
|
|
47
|
+
function avatarTint(palette, userId) {
|
|
48
|
+
let hash = 0;
|
|
49
|
+
for (let i = 0; i < userId.length; i++) hash = hash * 31 + userId.charCodeAt(i) | 0;
|
|
50
|
+
const key = AVATAR_TINT_KEYS[Math.abs(hash) % AVATAR_TINT_KEYS.length];
|
|
51
|
+
return palette[key] ?? palette.primary;
|
|
52
|
+
}
|
|
53
|
+
function swatch(theme, name) {
|
|
54
|
+
return theme.swatches[name] ?? theme.colors.primary;
|
|
55
|
+
}
|
|
56
|
+
function paperBorder(palette) {
|
|
57
|
+
return palette.borderSubtle;
|
|
58
|
+
}
|
|
59
|
+
function glowShadow(color, radius = 8, opacity = 0.4) {
|
|
60
|
+
return {
|
|
61
|
+
shadowColor: color,
|
|
62
|
+
shadowOffset: { width: 0, height: 0 },
|
|
63
|
+
shadowOpacity: opacity,
|
|
64
|
+
shadowRadius: radius,
|
|
65
|
+
elevation: 4
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function focusRingStyle(palette, width = 2) {
|
|
69
|
+
return { borderWidth: width, borderColor: palette.focus, borderStyle: "solid" };
|
|
70
|
+
}
|
|
71
|
+
function statusColor(palette, status, muted = false) {
|
|
72
|
+
if (muted) {
|
|
73
|
+
switch (status) {
|
|
74
|
+
case "success":
|
|
75
|
+
return palette.successMuted;
|
|
76
|
+
case "warning":
|
|
77
|
+
return palette.warningMuted;
|
|
78
|
+
case "danger":
|
|
79
|
+
return palette.dangerMuted;
|
|
80
|
+
default:
|
|
81
|
+
return palette.infoMuted;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
switch (status) {
|
|
85
|
+
case "success":
|
|
86
|
+
return palette.success;
|
|
87
|
+
case "warning":
|
|
88
|
+
return palette.warning;
|
|
89
|
+
case "danger":
|
|
90
|
+
return palette.danger;
|
|
91
|
+
default:
|
|
92
|
+
return palette.info;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
OctoSpacesThemeProvider,
|
|
97
|
+
avatarTint,
|
|
98
|
+
focusRingStyle,
|
|
99
|
+
glowShadow,
|
|
100
|
+
paperBorder,
|
|
101
|
+
presenceColor,
|
|
102
|
+
statusColor,
|
|
103
|
+
swatch,
|
|
104
|
+
useOctoSpacesTheme,
|
|
105
|
+
verificationColor
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme/provider.tsx","../src/theme/helpers.ts"],"sourcesContent":["/**\n * Theme injection plumbing — provider + hook.\n *\n * The package carries ZERO theme values. The host app builds a concrete {@link Theme}\n * and wraps its tree in `<OctoSpacesThemeProvider theme={resolvedTheme}>`.\n * All primitives then call `useOctoSpacesTheme()` to read the active theme.\n */\nimport React, { createContext, useContext } from 'react';\nimport type { Theme } from './types.js';\n\nconst ThemeContext = createContext<Theme | null>(null);\n\nexport interface OctoSpacesThemeProviderProps {\n theme: Theme;\n children: React.ReactNode;\n}\n\n/**\n * Wrap your root component with this provider to inject the resolved Theme into\n * every primitive from `@drakkar.software/octospaces-ui`.\n *\n * @example\n * ```tsx\n * import { OctoSpacesThemeProvider } from '@drakkar.software/octospaces-ui';\n * import { resolvedTheme } from '@/theme'; // your app's theme\n *\n * export default function App() {\n * return (\n * <OctoSpacesThemeProvider theme={resolvedTheme}>\n * <RootNavigator />\n * </OctoSpacesThemeProvider>\n * );\n * }\n * ```\n */\nexport function OctoSpacesThemeProvider({ theme, children }: OctoSpacesThemeProviderProps) {\n return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read the active theme. Throws if called outside an `<OctoSpacesThemeProvider>` —\n * this is intentional: a missing provider means primitives have no colors/spacing,\n * so a hard failure with a clear message is better than a silent rendering bug.\n */\nexport function useOctoSpacesTheme(): Theme {\n const theme = useContext(ThemeContext);\n if (!theme) {\n throw new Error(\n '[octospaces-ui] useOctoSpacesTheme() called outside of <OctoSpacesThemeProvider>. ' +\n 'Wrap your root component with <OctoSpacesThemeProvider theme={…}>.',\n );\n }\n return theme;\n}\n","/**\n * Pure palette-helper functions over a {@link Palette}. No theme values live here —\n * only functions that DERIVE from the injected palette (or from passed-in colors).\n *\n * Import these from `@drakkar.software/octospaces-ui` (re-exported by `src/index.ts`).\n */\nimport type { Palette, ShadowToken, Theme } from './types.js';\n\n// ── Presence ──────────────────────────────────────────────────────────────────\n\n/** Map a presence status string to the corresponding palette color. */\nexport function presenceColor(\n palette: Palette,\n status: 'online' | 'away' | 'busy' | 'offline' | string,\n): string {\n switch (status) {\n case 'online': return palette.presenceOnline;\n case 'away': return palette.presenceAway;\n case 'busy': return palette.presenceBusy;\n default: return palette.presenceOffline;\n }\n}\n\n// ── Verification ──────────────────────────────────────────────────────────────\n\n/** Map a verification level to the corresponding palette color. */\nexport function verificationColor(\n palette: Palette,\n level: 'verified' | 'partial' | 'none' | string,\n): string {\n switch (level) {\n case 'verified': return palette.verificationVerified;\n case 'partial': return palette.verificationPartial;\n default: return palette.verificationNone;\n }\n}\n\n// ── Avatar ────────────────────────────────────────────────────────────────────\n\nconst AVATAR_TINT_KEYS = [\n 'primary', 'success', 'warning', 'danger', 'info',\n] as const;\n\n/** Stable avatar background tint derived from a userId string. */\nexport function avatarTint(palette: Palette, userId: string): string {\n let hash = 0;\n for (let i = 0; i < userId.length; i++) hash = (hash * 31 + userId.charCodeAt(i)) | 0;\n const key = AVATAR_TINT_KEYS[Math.abs(hash) % AVATAR_TINT_KEYS.length];\n return (palette as unknown as Record<string, string>)[key] ?? palette.primary;\n}\n\n// ── Swatch ────────────────────────────────────────────────────────────────────\n\n/** Look up a named swatch; falls back to `palette.primary` if absent. */\nexport function swatch(theme: Theme, name: string): string {\n return theme.swatches[name] ?? theme.colors.primary;\n}\n\n// ── Borders ───────────────────────────────────────────────────────────────────\n\n/** Derive a `borderColor` value for a \"paper\" (elevated surface) border. */\nexport function paperBorder(palette: Palette): string {\n return palette.borderSubtle;\n}\n\n// ── Shadows ───────────────────────────────────────────────────────────────────\n\n/** Build a glow shadow token from a base color (used for focus rings, highlights). */\nexport function glowShadow(color: string, radius = 8, opacity = 0.4): ShadowToken {\n return {\n shadowColor: color,\n shadowOffset: { width: 0, height: 0 },\n shadowOpacity: opacity,\n shadowRadius: radius,\n elevation: 4,\n };\n}\n\n// ── Focus ring ────────────────────────────────────────────────────────────────\n\n/** Style object for a keyboard-focus indicator (web + React Native). */\nexport function focusRingStyle(\n palette: Palette,\n width = 2,\n): {\n borderWidth: number;\n borderColor: string;\n borderStyle: 'solid';\n} {\n return { borderWidth: width, borderColor: palette.focus, borderStyle: 'solid' };\n}\n\n// ── Status color ──────────────────────────────────────────────────────────────\n\n/** Map a semantic status name to its palette color. */\nexport function statusColor(\n palette: Palette,\n status: 'success' | 'warning' | 'danger' | 'info' | string,\n muted = false,\n): string {\n if (muted) {\n switch (status) {\n case 'success': return palette.successMuted;\n case 'warning': return palette.warningMuted;\n case 'danger': return palette.dangerMuted;\n default: return palette.infoMuted;\n }\n }\n switch (status) {\n case 'success': return palette.success;\n case 'warning': return palette.warning;\n case 'danger': return palette.danger;\n default: return palette.info;\n }\n}\n"],"mappings":";AAOA,OAAO,SAAS,eAAe,kBAAkB;AAGjD,IAAM,eAAe,cAA4B,IAAI;AAyB9C,SAAS,wBAAwB,EAAE,OAAO,SAAS,GAAiC;AACzF,SAAO,oCAAC,aAAa,UAAb,EAAsB,OAAO,SAAQ,QAAS;AACxD;AAOO,SAAS,qBAA4B;AAC1C,QAAM,QAAQ,WAAW,YAAY;AACrC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO;AACT;;;AC1CO,SAAS,cACd,SACA,QACQ;AACR,UAAQ,QAAQ;AAAA,IACd,KAAK;AAAU,aAAO,QAAQ;AAAA,IAC9B,KAAK;AAAU,aAAO,QAAQ;AAAA,IAC9B,KAAK;AAAU,aAAO,QAAQ;AAAA,IAC9B;AAAe,aAAO,QAAQ;AAAA,EAChC;AACF;AAKO,SAAS,kBACd,SACA,OACQ;AACR,UAAQ,OAAO;AAAA,IACb,KAAK;AAAY,aAAO,QAAQ;AAAA,IAChC,KAAK;AAAY,aAAO,QAAQ;AAAA,IAChC;AAAiB,aAAO,QAAQ;AAAA,EAClC;AACF;AAIA,IAAM,mBAAmB;AAAA,EACvB;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAAU;AAC7C;AAGO,SAAS,WAAW,SAAkB,QAAwB;AACnE,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,QAAQ,OAAO,KAAK,OAAO,WAAW,CAAC,IAAK;AACpF,QAAM,MAAM,iBAAiB,KAAK,IAAI,IAAI,IAAI,iBAAiB,MAAM;AACrE,SAAQ,QAA8C,GAAG,KAAK,QAAQ;AACxE;AAKO,SAAS,OAAO,OAAc,MAAsB;AACzD,SAAO,MAAM,SAAS,IAAI,KAAK,MAAM,OAAO;AAC9C;AAKO,SAAS,YAAY,SAA0B;AACpD,SAAO,QAAQ;AACjB;AAKO,SAAS,WAAW,OAAe,SAAS,GAAG,UAAU,KAAkB;AAChF,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc,EAAE,OAAO,GAAG,QAAQ,EAAE;AAAA,IACpC,eAAe;AAAA,IACf,cAAc;AAAA,IACd,WAAW;AAAA,EACb;AACF;AAKO,SAAS,eACd,SACA,QAAQ,GAKR;AACA,SAAO,EAAE,aAAa,OAAO,aAAa,QAAQ,OAAO,aAAa,QAAQ;AAChF;AAKO,SAAS,YACd,SACA,QACA,QAAQ,OACA;AACR,MAAI,OAAO;AACT,YAAQ,QAAQ;AAAA,MACd,KAAK;AAAW,eAAO,QAAQ;AAAA,MAC/B,KAAK;AAAW,eAAO,QAAQ;AAAA,MAC/B,KAAK;AAAW,eAAO,QAAQ;AAAA,MAC/B;AAAgB,eAAO,QAAQ;AAAA,IACjC;AAAA,EACF;AACA,UAAQ,QAAQ;AAAA,IACd,KAAK;AAAW,aAAO,QAAQ;AAAA,IAC/B,KAAK;AAAW,aAAO,QAAQ;AAAA,IAC/B,KAAK;AAAW,aAAO,QAAQ;AAAA,IAC/B;AAAgB,aAAO,QAAQ;AAAA,EACjC;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drakkar.software/octospaces-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared headless-themed UI primitives for OctoSpaces apps. Theme values are injected by the host app; this package ships only types + plumbing + primitives.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"react": ">=18",
|
|
17
|
+
"react-native": ">=0.75"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"@types/react": "^18.0.0",
|
|
22
|
+
"tsup": "^8.3.0",
|
|
23
|
+
"typescript": "~6.0.3",
|
|
24
|
+
"vitest": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"lint": "tsc --noEmit"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** @drakkar.software/octospaces-ui — public surface */
|
|
2
|
+
|
|
3
|
+
// Theme plumbing — inject the host app's resolved Theme via provider, read it via hook.
|
|
4
|
+
export { OctoSpacesThemeProvider, useOctoSpacesTheme } from './theme/provider.js';
|
|
5
|
+
export type { OctoSpacesThemeProviderProps } from './theme/provider.js';
|
|
6
|
+
|
|
7
|
+
// Theme types — host apps use these to type their resolved Theme object.
|
|
8
|
+
export type {
|
|
9
|
+
ColorScheme,
|
|
10
|
+
Palette,
|
|
11
|
+
Theme,
|
|
12
|
+
Spacing,
|
|
13
|
+
Radii,
|
|
14
|
+
TypeScale,
|
|
15
|
+
Typography,
|
|
16
|
+
Fonts,
|
|
17
|
+
Easing,
|
|
18
|
+
MotionToken,
|
|
19
|
+
Motion,
|
|
20
|
+
ShadowToken,
|
|
21
|
+
Shadows,
|
|
22
|
+
Layout,
|
|
23
|
+
Opacity,
|
|
24
|
+
Swatches,
|
|
25
|
+
Layers,
|
|
26
|
+
LabelTracking,
|
|
27
|
+
} from './theme/types.js';
|
|
28
|
+
|
|
29
|
+
// Pure palette helpers — import and call with a Palette (or Theme) from useOctoSpacesTheme().
|
|
30
|
+
export {
|
|
31
|
+
presenceColor,
|
|
32
|
+
verificationColor,
|
|
33
|
+
avatarTint,
|
|
34
|
+
swatch,
|
|
35
|
+
paperBorder,
|
|
36
|
+
glowShadow,
|
|
37
|
+
focusRingStyle,
|
|
38
|
+
statusColor,
|
|
39
|
+
} from './theme/helpers.js';
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { Palette, Theme } from './types.js';
|
|
3
|
+
import {
|
|
4
|
+
avatarTint,
|
|
5
|
+
focusRingStyle,
|
|
6
|
+
glowShadow,
|
|
7
|
+
paperBorder,
|
|
8
|
+
presenceColor,
|
|
9
|
+
statusColor,
|
|
10
|
+
swatch,
|
|
11
|
+
verificationColor,
|
|
12
|
+
} from './helpers.js';
|
|
13
|
+
|
|
14
|
+
const mockPalette: Palette = {
|
|
15
|
+
background: '#000',
|
|
16
|
+
surface: '#111',
|
|
17
|
+
surfaceElevated: '#222',
|
|
18
|
+
surfaceModal: '#333',
|
|
19
|
+
surfaceInput: '#444',
|
|
20
|
+
sidebar: '#555',
|
|
21
|
+
sidebarActive: '#666',
|
|
22
|
+
border: '#777',
|
|
23
|
+
borderSubtle: '#888',
|
|
24
|
+
borderStrong: '#999',
|
|
25
|
+
text: '#fff',
|
|
26
|
+
textSecondary: '#eee',
|
|
27
|
+
textTertiary: '#ddd',
|
|
28
|
+
textDisabled: '#ccc',
|
|
29
|
+
textInverse: '#000',
|
|
30
|
+
textOnPrimary: '#fff',
|
|
31
|
+
primary: '#6366f1',
|
|
32
|
+
primaryHover: '#4f46e5',
|
|
33
|
+
primaryMuted: '#312e81',
|
|
34
|
+
primarySubtle: '#1e1b4b',
|
|
35
|
+
success: '#22c55e',
|
|
36
|
+
successMuted: '#14532d',
|
|
37
|
+
warning: '#f59e0b',
|
|
38
|
+
warningMuted: '#78350f',
|
|
39
|
+
danger: '#ef4444',
|
|
40
|
+
dangerMuted: '#7f1d1d',
|
|
41
|
+
info: '#3b82f6',
|
|
42
|
+
infoMuted: '#1e3a5f',
|
|
43
|
+
presenceOnline: '#22c55e',
|
|
44
|
+
presenceAway: '#f59e0b',
|
|
45
|
+
presenceBusy: '#ef4444',
|
|
46
|
+
presenceOffline: '#6b7280',
|
|
47
|
+
verificationVerified: '#22c55e',
|
|
48
|
+
verificationPartial: '#f59e0b',
|
|
49
|
+
verificationNone: '#6b7280',
|
|
50
|
+
overlay: 'rgba(0,0,0,0.5)',
|
|
51
|
+
shadow: '#000',
|
|
52
|
+
focus: '#6366f1',
|
|
53
|
+
skeleton: '#333',
|
|
54
|
+
skeletonShimmer: '#444',
|
|
55
|
+
editorCanvas: '#1a1a2e',
|
|
56
|
+
tooltipBg: '#222',
|
|
57
|
+
onTooltip: '#fff',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const mockTheme: Theme = {
|
|
61
|
+
scheme: 'dark',
|
|
62
|
+
colors: mockPalette,
|
|
63
|
+
spacing: { '0': 0, '1': 4, '2': 8, '3': 12, '4': 16 },
|
|
64
|
+
radii: { sm: 4, md: 8, lg: 16, full: 9999 },
|
|
65
|
+
type: {},
|
|
66
|
+
fonts: {},
|
|
67
|
+
motion: {},
|
|
68
|
+
shadows: {},
|
|
69
|
+
layout: {},
|
|
70
|
+
opacity: { disabled: 0.5 },
|
|
71
|
+
swatches: { blue: '#3b82f6', green: '#22c55e', red: '#ef4444' },
|
|
72
|
+
layers: { modal: 100, tooltip: 200 },
|
|
73
|
+
easing: { standard: [0.4, 0, 0.2, 1] },
|
|
74
|
+
labelTracking: {},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
describe('presenceColor', () => {
|
|
78
|
+
it('maps online → presenceOnline', () => {
|
|
79
|
+
expect(presenceColor(mockPalette, 'online')).toBe(mockPalette.presenceOnline);
|
|
80
|
+
});
|
|
81
|
+
it('maps away → presenceAway', () => {
|
|
82
|
+
expect(presenceColor(mockPalette, 'away')).toBe(mockPalette.presenceAway);
|
|
83
|
+
});
|
|
84
|
+
it('maps busy → presenceBusy', () => {
|
|
85
|
+
expect(presenceColor(mockPalette, 'busy')).toBe(mockPalette.presenceBusy);
|
|
86
|
+
});
|
|
87
|
+
it('falls back to offline for unknown status', () => {
|
|
88
|
+
expect(presenceColor(mockPalette, 'unknown')).toBe(mockPalette.presenceOffline);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('verificationColor', () => {
|
|
93
|
+
it('maps verified', () => {
|
|
94
|
+
expect(verificationColor(mockPalette, 'verified')).toBe(mockPalette.verificationVerified);
|
|
95
|
+
});
|
|
96
|
+
it('maps partial', () => {
|
|
97
|
+
expect(verificationColor(mockPalette, 'partial')).toBe(mockPalette.verificationPartial);
|
|
98
|
+
});
|
|
99
|
+
it('falls back to none', () => {
|
|
100
|
+
expect(verificationColor(mockPalette, 'unknown')).toBe(mockPalette.verificationNone);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('avatarTint', () => {
|
|
105
|
+
it('returns a string color', () => {
|
|
106
|
+
expect(typeof avatarTint(mockPalette, 'user123')).toBe('string');
|
|
107
|
+
});
|
|
108
|
+
it('is stable for the same userId', () => {
|
|
109
|
+
expect(avatarTint(mockPalette, 'alice')).toBe(avatarTint(mockPalette, 'alice'));
|
|
110
|
+
});
|
|
111
|
+
it('differs for different userIds', () => {
|
|
112
|
+
// Not guaranteed to differ, but very likely
|
|
113
|
+
const colors = new Set(['alice', 'bob', 'carol', 'dave', 'eve'].map(u => avatarTint(mockPalette, u)));
|
|
114
|
+
expect(colors.size).toBeGreaterThan(1);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('swatch', () => {
|
|
119
|
+
it('returns the named swatch', () => {
|
|
120
|
+
expect(swatch(mockTheme, 'blue')).toBe('#3b82f6');
|
|
121
|
+
});
|
|
122
|
+
it('falls back to primary for unknown swatch', () => {
|
|
123
|
+
expect(swatch(mockTheme, 'nonexistent')).toBe(mockPalette.primary);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('paperBorder', () => {
|
|
128
|
+
it('returns borderSubtle', () => {
|
|
129
|
+
expect(paperBorder(mockPalette)).toBe(mockPalette.borderSubtle);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('glowShadow', () => {
|
|
134
|
+
it('returns a shadow token with the given color', () => {
|
|
135
|
+
const shadow = glowShadow('#6366f1');
|
|
136
|
+
expect(shadow.shadowColor).toBe('#6366f1');
|
|
137
|
+
expect(typeof shadow.shadowRadius).toBe('number');
|
|
138
|
+
});
|
|
139
|
+
it('respects custom radius and opacity', () => {
|
|
140
|
+
const shadow = glowShadow('#fff', 12, 0.6);
|
|
141
|
+
expect(shadow.shadowRadius).toBe(12);
|
|
142
|
+
expect(shadow.shadowOpacity).toBe(0.6);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('focusRingStyle', () => {
|
|
147
|
+
it('returns an object with borderColor matching palette.focus', () => {
|
|
148
|
+
const ring = focusRingStyle(mockPalette);
|
|
149
|
+
expect(ring.borderColor).toBe(mockPalette.focus);
|
|
150
|
+
expect(ring.borderStyle).toBe('solid');
|
|
151
|
+
expect(ring.borderWidth).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
it('respects custom width', () => {
|
|
154
|
+
expect(focusRingStyle(mockPalette, 3).borderWidth).toBe(3);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('statusColor', () => {
|
|
159
|
+
it('returns success color', () => {
|
|
160
|
+
expect(statusColor(mockPalette, 'success')).toBe(mockPalette.success);
|
|
161
|
+
});
|
|
162
|
+
it('returns muted variant when muted=true', () => {
|
|
163
|
+
expect(statusColor(mockPalette, 'danger', true)).toBe(mockPalette.dangerMuted);
|
|
164
|
+
});
|
|
165
|
+
it('falls back to info for unknown status', () => {
|
|
166
|
+
expect(statusColor(mockPalette, 'unknown')).toBe(mockPalette.info);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure palette-helper functions over a {@link Palette}. No theme values live here —
|
|
3
|
+
* only functions that DERIVE from the injected palette (or from passed-in colors).
|
|
4
|
+
*
|
|
5
|
+
* Import these from `@drakkar.software/octospaces-ui` (re-exported by `src/index.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import type { Palette, ShadowToken, Theme } from './types.js';
|
|
8
|
+
|
|
9
|
+
// ── Presence ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Map a presence status string to the corresponding palette color. */
|
|
12
|
+
export function presenceColor(
|
|
13
|
+
palette: Palette,
|
|
14
|
+
status: 'online' | 'away' | 'busy' | 'offline' | string,
|
|
15
|
+
): string {
|
|
16
|
+
switch (status) {
|
|
17
|
+
case 'online': return palette.presenceOnline;
|
|
18
|
+
case 'away': return palette.presenceAway;
|
|
19
|
+
case 'busy': return palette.presenceBusy;
|
|
20
|
+
default: return palette.presenceOffline;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Verification ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Map a verification level to the corresponding palette color. */
|
|
27
|
+
export function verificationColor(
|
|
28
|
+
palette: Palette,
|
|
29
|
+
level: 'verified' | 'partial' | 'none' | string,
|
|
30
|
+
): string {
|
|
31
|
+
switch (level) {
|
|
32
|
+
case 'verified': return palette.verificationVerified;
|
|
33
|
+
case 'partial': return palette.verificationPartial;
|
|
34
|
+
default: return palette.verificationNone;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Avatar ────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const AVATAR_TINT_KEYS = [
|
|
41
|
+
'primary', 'success', 'warning', 'danger', 'info',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
/** Stable avatar background tint derived from a userId string. */
|
|
45
|
+
export function avatarTint(palette: Palette, userId: string): string {
|
|
46
|
+
let hash = 0;
|
|
47
|
+
for (let i = 0; i < userId.length; i++) hash = (hash * 31 + userId.charCodeAt(i)) | 0;
|
|
48
|
+
const key = AVATAR_TINT_KEYS[Math.abs(hash) % AVATAR_TINT_KEYS.length];
|
|
49
|
+
return (palette as unknown as Record<string, string>)[key] ?? palette.primary;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Swatch ────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Look up a named swatch; falls back to `palette.primary` if absent. */
|
|
55
|
+
export function swatch(theme: Theme, name: string): string {
|
|
56
|
+
return theme.swatches[name] ?? theme.colors.primary;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Borders ───────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** Derive a `borderColor` value for a "paper" (elevated surface) border. */
|
|
62
|
+
export function paperBorder(palette: Palette): string {
|
|
63
|
+
return palette.borderSubtle;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Shadows ───────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/** Build a glow shadow token from a base color (used for focus rings, highlights). */
|
|
69
|
+
export function glowShadow(color: string, radius = 8, opacity = 0.4): ShadowToken {
|
|
70
|
+
return {
|
|
71
|
+
shadowColor: color,
|
|
72
|
+
shadowOffset: { width: 0, height: 0 },
|
|
73
|
+
shadowOpacity: opacity,
|
|
74
|
+
shadowRadius: radius,
|
|
75
|
+
elevation: 4,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Focus ring ────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/** Style object for a keyboard-focus indicator (web + React Native). */
|
|
82
|
+
export function focusRingStyle(
|
|
83
|
+
palette: Palette,
|
|
84
|
+
width = 2,
|
|
85
|
+
): {
|
|
86
|
+
borderWidth: number;
|
|
87
|
+
borderColor: string;
|
|
88
|
+
borderStyle: 'solid';
|
|
89
|
+
} {
|
|
90
|
+
return { borderWidth: width, borderColor: palette.focus, borderStyle: 'solid' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Status color ──────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** Map a semantic status name to its palette color. */
|
|
96
|
+
export function statusColor(
|
|
97
|
+
palette: Palette,
|
|
98
|
+
status: 'success' | 'warning' | 'danger' | 'info' | string,
|
|
99
|
+
muted = false,
|
|
100
|
+
): string {
|
|
101
|
+
if (muted) {
|
|
102
|
+
switch (status) {
|
|
103
|
+
case 'success': return palette.successMuted;
|
|
104
|
+
case 'warning': return palette.warningMuted;
|
|
105
|
+
case 'danger': return palette.dangerMuted;
|
|
106
|
+
default: return palette.infoMuted;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
switch (status) {
|
|
110
|
+
case 'success': return palette.success;
|
|
111
|
+
case 'warning': return palette.warning;
|
|
112
|
+
case 'danger': return palette.danger;
|
|
113
|
+
default: return palette.info;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme injection plumbing — provider + hook.
|
|
3
|
+
*
|
|
4
|
+
* The package carries ZERO theme values. The host app builds a concrete {@link Theme}
|
|
5
|
+
* and wraps its tree in `<OctoSpacesThemeProvider theme={resolvedTheme}>`.
|
|
6
|
+
* All primitives then call `useOctoSpacesTheme()` to read the active theme.
|
|
7
|
+
*/
|
|
8
|
+
import React, { createContext, useContext } from 'react';
|
|
9
|
+
import type { Theme } from './types.js';
|
|
10
|
+
|
|
11
|
+
const ThemeContext = createContext<Theme | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface OctoSpacesThemeProviderProps {
|
|
14
|
+
theme: Theme;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap your root component with this provider to inject the resolved Theme into
|
|
20
|
+
* every primitive from `@drakkar.software/octospaces-ui`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import { OctoSpacesThemeProvider } from '@drakkar.software/octospaces-ui';
|
|
25
|
+
* import { resolvedTheme } from '@/theme'; // your app's theme
|
|
26
|
+
*
|
|
27
|
+
* export default function App() {
|
|
28
|
+
* return (
|
|
29
|
+
* <OctoSpacesThemeProvider theme={resolvedTheme}>
|
|
30
|
+
* <RootNavigator />
|
|
31
|
+
* </OctoSpacesThemeProvider>
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function OctoSpacesThemeProvider({ theme, children }: OctoSpacesThemeProviderProps) {
|
|
37
|
+
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read the active theme. Throws if called outside an `<OctoSpacesThemeProvider>` —
|
|
42
|
+
* this is intentional: a missing provider means primitives have no colors/spacing,
|
|
43
|
+
* so a hard failure with a clear message is better than a silent rendering bug.
|
|
44
|
+
*/
|
|
45
|
+
export function useOctoSpacesTheme(): Theme {
|
|
46
|
+
const theme = useContext(ThemeContext);
|
|
47
|
+
if (!theme) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'[octospaces-ui] useOctoSpacesTheme() called outside of <OctoSpacesThemeProvider>. ' +
|
|
50
|
+
'Wrap your root component with <OctoSpacesThemeProvider theme={…}>.',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return theme;
|
|
54
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme contract for `@drakkar.software/octospaces-ui`.
|
|
3
|
+
*
|
|
4
|
+
* The package ships NO theme VALUES — only these type definitions. The host app
|
|
5
|
+
* constructs a concrete {@link Theme} object (with its own palette, tokens, and
|
|
6
|
+
* scheme logic) and passes it to `<OctoSpacesThemeProvider theme={…}>` at the root.
|
|
7
|
+
*
|
|
8
|
+
* The {@link Palette} interface is the SUPERSET of OctoChat + OctoVault so the UI
|
|
9
|
+
* primitives work correctly in both apps. Apps that don't use vault-specific keys
|
|
10
|
+
* (like `editorCanvas`) can safely set them to a sensible default.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The active color scheme. Passed as part of `Theme.scheme`. */
|
|
14
|
+
export type ColorScheme = 'light' | 'dark';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Full color palette contract. OctoVault's palette is the superset. OctoChat apps
|
|
18
|
+
* add `editorCanvas`, `tooltipBg`, `onTooltip` as they migrate to the shared UI.
|
|
19
|
+
*
|
|
20
|
+
* All colors are CSS/RN color strings (hex, rgba, or named color tokens).
|
|
21
|
+
*/
|
|
22
|
+
export interface Palette {
|
|
23
|
+
// ── Background layers ─────────────────────────────────────────────────────
|
|
24
|
+
background: string;
|
|
25
|
+
surface: string;
|
|
26
|
+
surfaceElevated: string;
|
|
27
|
+
surfaceModal: string;
|
|
28
|
+
surfaceInput: string;
|
|
29
|
+
sidebar: string;
|
|
30
|
+
sidebarActive: string;
|
|
31
|
+
|
|
32
|
+
// ── Borders + dividers ────────────────────────────────────────────────────
|
|
33
|
+
border: string;
|
|
34
|
+
borderSubtle: string;
|
|
35
|
+
borderStrong: string;
|
|
36
|
+
|
|
37
|
+
// ── Text ─────────────────────────────────────────────────────────────────
|
|
38
|
+
text: string;
|
|
39
|
+
textSecondary: string;
|
|
40
|
+
textTertiary: string;
|
|
41
|
+
textDisabled: string;
|
|
42
|
+
textInverse: string;
|
|
43
|
+
textOnPrimary: string;
|
|
44
|
+
|
|
45
|
+
// ── Brand + interactive ───────────────────────────────────────────────────
|
|
46
|
+
primary: string;
|
|
47
|
+
primaryHover: string;
|
|
48
|
+
primaryMuted: string;
|
|
49
|
+
primarySubtle: string;
|
|
50
|
+
|
|
51
|
+
// ── Semantic ──────────────────────────────────────────────────────────────
|
|
52
|
+
success: string;
|
|
53
|
+
successMuted: string;
|
|
54
|
+
warning: string;
|
|
55
|
+
warningMuted: string;
|
|
56
|
+
danger: string;
|
|
57
|
+
dangerMuted: string;
|
|
58
|
+
info: string;
|
|
59
|
+
infoMuted: string;
|
|
60
|
+
|
|
61
|
+
// ── Presence + verification ───────────────────────────────────────────────
|
|
62
|
+
presenceOnline: string;
|
|
63
|
+
presenceAway: string;
|
|
64
|
+
presenceBusy: string;
|
|
65
|
+
presenceOffline: string;
|
|
66
|
+
|
|
67
|
+
verificationVerified: string;
|
|
68
|
+
verificationPartial: string;
|
|
69
|
+
verificationNone: string;
|
|
70
|
+
|
|
71
|
+
// ── Misc ──────────────────────────────────────────────────────────────────
|
|
72
|
+
overlay: string;
|
|
73
|
+
shadow: string;
|
|
74
|
+
focus: string;
|
|
75
|
+
skeleton: string;
|
|
76
|
+
skeletonShimmer: string;
|
|
77
|
+
|
|
78
|
+
// ── Vault-specific extras (superset) ─────────────────────────────────────
|
|
79
|
+
editorCanvas: string;
|
|
80
|
+
tooltipBg: string;
|
|
81
|
+
onTooltip: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Static spacing scale. Host app provides an object with numeric pixel values
|
|
86
|
+
* keyed by token name. E.g. `{ '0': 0, '1': 4, '2': 8, '3': 12, '4': 16, … }`.
|
|
87
|
+
*/
|
|
88
|
+
export type Spacing = Record<string, number>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Border-radius scale.
|
|
92
|
+
*/
|
|
93
|
+
export type Radii = Record<string, number | string>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Typography scale: font sizes, line heights, weights — keyed by variant name.
|
|
97
|
+
*/
|
|
98
|
+
export interface TypeScale {
|
|
99
|
+
size: number;
|
|
100
|
+
lineHeight: number;
|
|
101
|
+
weight?: string | number;
|
|
102
|
+
letterSpacing?: number;
|
|
103
|
+
}
|
|
104
|
+
export type Typography = Record<string, TypeScale>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Font family config. Keys are semantic names; values are font-family strings.
|
|
108
|
+
*/
|
|
109
|
+
export type Fonts = Record<string, string>;
|
|
110
|
+
|
|
111
|
+
/** Easing curve definitions (e.g. for Reanimated `withTiming`). */
|
|
112
|
+
export type Easing = Record<string, number[]>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Motion token: duration + easing pairs keyed by animation name.
|
|
116
|
+
*/
|
|
117
|
+
export interface MotionToken {
|
|
118
|
+
duration: number;
|
|
119
|
+
easing?: number[];
|
|
120
|
+
}
|
|
121
|
+
export type Motion = Record<string, MotionToken>;
|
|
122
|
+
|
|
123
|
+
/** Shadow / elevation definitions. */
|
|
124
|
+
export interface ShadowToken {
|
|
125
|
+
shadowColor?: string;
|
|
126
|
+
shadowOffset?: { width: number; height: number };
|
|
127
|
+
shadowOpacity?: number;
|
|
128
|
+
shadowRadius?: number;
|
|
129
|
+
elevation?: number;
|
|
130
|
+
}
|
|
131
|
+
export type Shadows = Record<string, ShadowToken>;
|
|
132
|
+
|
|
133
|
+
/** Layout constants (screen margins, nav heights, etc.). */
|
|
134
|
+
export type Layout = Record<string, number>;
|
|
135
|
+
|
|
136
|
+
/** Opacity scale. */
|
|
137
|
+
export type Opacity = Record<string, number>;
|
|
138
|
+
|
|
139
|
+
/** Named color swatches (beyond the palette — brand accents, label colors). */
|
|
140
|
+
export type Swatches = Record<string, string>;
|
|
141
|
+
|
|
142
|
+
/** z-index layers. */
|
|
143
|
+
export type Layers = Record<string, number>;
|
|
144
|
+
|
|
145
|
+
/** Letter-spacing presets keyed by variant name. */
|
|
146
|
+
export type LabelTracking = Record<string, number>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The complete Theme object the host app constructs and injects.
|
|
150
|
+
* All primitives in this package read ONLY from this injected Theme.
|
|
151
|
+
*/
|
|
152
|
+
export interface Theme {
|
|
153
|
+
scheme: ColorScheme;
|
|
154
|
+
colors: Palette;
|
|
155
|
+
spacing: Spacing;
|
|
156
|
+
radii: Radii;
|
|
157
|
+
type: Typography;
|
|
158
|
+
fonts: Fonts;
|
|
159
|
+
motion: Motion;
|
|
160
|
+
shadows: Shadows;
|
|
161
|
+
layout: Layout;
|
|
162
|
+
opacity: Opacity;
|
|
163
|
+
swatches: Swatches;
|
|
164
|
+
layers: Layers;
|
|
165
|
+
easing: Easing;
|
|
166
|
+
labelTracking: LabelTracking;
|
|
167
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"lib": ["ES2020", "DOM"],
|
|
8
|
+
"jsx": "react-native",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: { index: 'src/index.ts' },
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: true,
|
|
8
|
+
splitting: false,
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
target: 'es2020',
|
|
11
|
+
external: ['react', 'react-native'],
|
|
12
|
+
});
|