@ompchamber/ui 1.0.0 → 2.0.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/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@ompchamber/ui",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Preact hooks and components for OMPChamber panel plugins.",
6
6
  "exports": {
7
7
  ".": "./src/index.tsx",
8
- "./components": "./src/components.tsx",
9
- "./styles.css": "./src/styles.css"
8
+ "./components": "./src/components.tsx"
10
9
  },
11
10
  "files": [
12
11
  "src"
@@ -6,17 +6,19 @@
6
6
  /**
7
7
  * The panel UI kit.
8
8
  *
9
- * Components a panel draws with, styled by the CSS variables the host's palette
10
- * drives. They exist so panels look like the chamber and like each other without
11
- * each one re-deriving the same markup — a second copy of a labelled field is
12
- * the first place two panels drift apart.
9
+ * These are ordinary host components, not a sandboxed mini-app's widgets: a
10
+ * plugin renders inside the chamber's own tree, so the kit uses the chamber's
11
+ * theme variables and Tailwind utilities directly and looks native in every
12
+ * palette with no CSS bridge.
13
13
  *
14
- * Class names are prefixed `oc-` and the stylesheet ships from the same package
15
- * (`@ompchamber/ui/styles.css`), so a plugin imports one thing and gets both.
16
- * Colours come from `var(--oc-*)`, which the host does NOT set: the frame has an
17
- * opaque origin, so the palette arrives as `data-theme` on `<html>` and the
18
- * stylesheet maps it. That is why this package carries CSS rather than reading
19
- * the app's.
14
+ * The kit exists so a plugin does not re-invent the same six shapes — a titled
15
+ * panel, a label/value row, a text field, a button, an empty state, a note —
16
+ * and so those shapes stay consistent across plugins the way they are within
17
+ * the chamber itself.
18
+ *
19
+ * Every component takes its colours from the theme variables (`bg-paper`,
20
+ * `text-ink`, `border-ink/10`), never a literal, which is what keeps a plugin
21
+ * legible in the light and dark palettes without knowing they exist.
20
22
  */
21
23
 
22
24
  import type { ComponentChildren } from 'preact';
@@ -29,11 +31,11 @@ interface PanelProps {
29
31
  /** A panel's outer chrome: a title bar over a scrolling body. */
30
32
  export function Panel({ title, children }: PanelProps) {
31
33
  return (
32
- <div class="oc-panel">
33
- <header class="oc-panel-header">
34
- <h1 class="oc-panel-title">{title}</h1>
35
- </header>
36
- <div class="oc-panel-body">{children}</div>
34
+ <div class="flex flex-col h-full w-full bg-paper text-ink">
35
+ <div class="flex-shrink-0 border-b border-ink/10 px-3 py-2">
36
+ <span class="text-xs font-semibold text-ink">{title}</span>
37
+ </div>
38
+ <div class="flex-1 min-h-0 overflow-auto p-3">{children}</div>
37
39
  </div>
38
40
  );
39
41
  }
@@ -41,60 +43,55 @@ export function Panel({ title, children }: PanelProps) {
41
43
  interface FieldProps {
42
44
  label: string;
43
45
  value: string;
44
- /** Shown under the value, for a path or an id that would otherwise wrap badly. */
45
46
  hint?: string;
46
47
  }
47
48
 
48
49
  /** One labelled fact. The label is a micro-header, the value is machine text. */
49
50
  export function Field({ label, value, hint }: FieldProps) {
50
51
  return (
51
- <div class="oc-field">
52
- <span class="oc-label">{label}</span>
53
- <span class="oc-value" title={value}>
54
- {value}
55
- </span>
56
- {hint ? <span class="oc-hint">{hint}</span> : null}
52
+ <div class="flex flex-col gap-0.5">
53
+ <span class="text-[10px] uppercase font-mono text-ink/40">{label}</span>
54
+ <span class="text-xs font-mono text-ink break-all">{value}</span>
55
+ {hint ? <span class="text-[11px] text-ink/50">{hint}</span> : null}
57
56
  </div>
58
57
  );
59
58
  }
60
59
 
61
60
  /** A stack of `Field`s. */
62
61
  export function FieldList({ children }: { children: ComponentChildren }) {
63
- return <div class="oc-fields">{children}</div>;
62
+ return <div class="flex flex-col gap-3">{children}</div>;
64
63
  }
65
64
 
66
65
  interface TextAreaFieldProps {
67
- id?: string;
66
+ id: string;
68
67
  label: string;
68
+ value: string;
69
69
  placeholder?: string;
70
- /** `null` while the stored value has not been read yet. */
71
- value: string | null;
72
70
  hint?: string;
73
- onInput: (next: string) => void;
71
+ onInput: (value: string) => void;
74
72
  }
75
73
 
76
74
  /**
77
75
  * A labelled textarea.
78
76
  *
79
- * Disabled while `value` is null — that is "the read has not answered yet", and
80
- * an editable box there would let the user type into a field whose contents are
81
- * about to be replaced by the stored value.
77
+ * Controlled through `onInput` rather than owning its own state, so the caller
78
+ * decides what a keystroke means — the session-persisting hook debounces the
79
+ * write, while a purely local field can keep the value in the caller.
82
80
  */
83
- export function TextAreaField({ id, label, placeholder, value, hint, onInput }: TextAreaFieldProps) {
81
+ export function TextAreaField({ id, label, value, placeholder, hint, onInput }: TextAreaFieldProps) {
84
82
  return (
85
- <div class="oc-textarea-field">
86
- <label class="oc-label" for={id}>
83
+ <div class="flex flex-col gap-1">
84
+ <label for={id} class="text-[10px] uppercase font-mono text-ink/40">
87
85
  {label}
88
86
  </label>
89
87
  <textarea
90
88
  id={id}
91
- class="oc-textarea"
89
+ value={value}
92
90
  placeholder={placeholder}
93
- value={value ?? ''}
94
- disabled={value === null}
95
- onInput={(event) => onInput(event.currentTarget.value)}
91
+ onInput={(event) => onInput((event.currentTarget as HTMLTextAreaElement).value)}
92
+ class="w-full min-h-20 resize-y bg-canvas border border-ink/15 rounded px-2.5 py-1.5 text-xs font-mono text-ink placeholder-ink/35 focus:outline-none focus:border-ink/40"
96
93
  />
97
- {hint ? <span class="oc-hint">{hint}</span> : null}
94
+ {hint ? <span class="text-[11px] text-ink/50">{hint}</span> : null}
98
95
  </div>
99
96
  );
100
97
  }
@@ -103,13 +100,23 @@ interface ButtonProps {
103
100
  children: ComponentChildren;
104
101
  onClick: () => void;
105
102
  disabled?: boolean;
103
+ /** The one action a panel wants the user to take. */
106
104
  variant?: 'default' | 'primary';
107
105
  }
108
106
 
109
- /** A button. `primary` is the one action a panel wants the user to take. */
107
+ /** A button. */
110
108
  export function Button({ children, onClick, disabled, variant = 'default' }: ButtonProps) {
109
+ const tone =
110
+ variant === 'primary'
111
+ ? 'bg-ink text-canvas border-ink hover:opacity-90'
112
+ : 'border-ink/15 hover:bg-ink/5 text-ink';
111
113
  return (
112
- <button type="button" class={`oc-button oc-button-${variant}`} disabled={disabled} onClick={onClick}>
114
+ <button
115
+ type="button"
116
+ onClick={onClick}
117
+ disabled={disabled}
118
+ class={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded border transition-colors disabled:opacity-40 disabled:cursor-default ${tone}`}
119
+ >
113
120
  {children}
114
121
  </button>
115
122
  );
@@ -117,10 +124,15 @@ export function Button({ children, onClick, disabled, variant = 'default' }: But
117
124
 
118
125
  /** Centred placeholder for a panel with nothing to show. */
119
126
  export function Empty({ children }: { children: ComponentChildren }) {
120
- return <p class="oc-empty">{children}</p>;
127
+ return (
128
+ <div class="h-full flex items-center justify-center px-6 text-center">
129
+ <p class="text-xs text-ink/45 font-mono">{children}</p>
130
+ </div>
131
+ );
121
132
  }
122
133
 
123
134
  /** A message. `error` is the only chroma a panel should use. */
124
135
  export function Note({ children, tone = 'default' }: { children: ComponentChildren; tone?: 'default' | 'error' }) {
125
- return <p class={`oc-note oc-note-${tone}`}>{children}</p>;
136
+ const colour = tone === 'error' ? 'text-error' : 'text-ink/60';
137
+ return <p class={`text-[11px] ${colour}`}>{children}</p>;
126
138
  }
package/src/index.tsx CHANGED
@@ -4,54 +4,85 @@
4
4
  */
5
5
 
6
6
  /**
7
- * The Preact integration for a panel plugin: one provider, four hooks.
7
+ * The UI kit a plugin renders with, and the host services it reads through.
8
8
  *
9
- * A plugin used to hand-roll this — a context, a `ready` await, a theme handler,
10
- * a debounced session write — which meant every plugin re-derived the same two
11
- * behaviours that are easy to get wrong:
9
+ * Everything here runs IN THE HOST'S TREE — same Preact instance, same
10
+ * document, same theme — so the kit is plain Preact components, not a
11
+ * message-passing client. What it still must not do is reach into the host's
12
+ * own source: this is a published package, so the host INJECTS its services
13
+ * once at boot via `configureUiKit`, and the hooks read them.
12
14
  *
13
- * - **The provider owns BOTH bridge subscriptions.** Registering `onTheme` from
14
- * a component only fires if that component mounts; a panel whose components
15
- * never called `useTheme()` silently stopped following the palette.
16
- * - **The context arrives late.** `workspacePath` and `sessionId` are resolved
17
- * asynchronously by the chamber, so they are usually empty when `ready`
18
- * settles. A component that captured `info` once would show "none" over a real
19
- * workspace.
15
+ * Two rules the hooks exist to enforce, because a plugin gets them wrong:
20
16
  *
21
- * Both are handled here so a plugin cannot get them wrong.
17
+ * - **Context arrives late.** The active session and workspace are resolved
18
+ * asynchronously, so a component that captured them once would show "none"
19
+ * over a real workspace. Every hook subscribes and re-renders.
20
+ * - **Session state is not component state.** The component is unmounted when
21
+ * its panel is hidden, so a value kept in `useState` alone would be lost on
22
+ * every tab switch. `useSessionValue` reads the chamber's store.
22
23
  */
23
24
 
24
- import { createContext } from 'preact';
25
- import { useContext, useEffect, useState } from 'preact/hooks';
26
- import type { ComponentChildren } from 'preact';
27
- import { acquirePanel, type ChamberPanelApi, type PanelInfo } from '@ompchamber/plugin-sdk';
25
+ import { useCallback, useEffect, useState } from 'preact/hooks';
28
26
 
29
- interface PanelContextValue {
30
- api: ChamberPanelApi;
31
- info: PanelInfo;
27
+ /** What the host seeds every plugin component with. */
28
+ export interface PanelContext {
29
+ sessionId: string | null;
30
+ workspacePath: string | null;
31
+ theme: string;
32
32
  }
33
33
 
34
- const PanelContext = createContext<PanelContextValue | null>(null);
34
+ /**
35
+ * The host's own services, injected once.
36
+ *
37
+ * Deliberately narrow: each field is something a plugin genuinely cannot do
38
+ * itself (read the chamber's per-session store, know the live palette, resolve
39
+ * the active workspace). Anything a plugin could read from the DOM it should.
40
+ */
41
+ export interface UiKitServices {
42
+ /** The current panel context. */
43
+ context(): PanelContext;
44
+ /** Subscribe to context changes (a new session, a palette switch). */
45
+ subscribe(listener: (context: PanelContext) => void): () => void;
46
+ /** Read one per-session value. */
47
+ getSessionValue(sessionId: string | null, key: string): string | null;
48
+ /** Write one per-session value. */
49
+ setSessionValue(sessionId: string | null, key: string, value: string): void;
50
+ /** Read a text file inside the active workspace. Rejects with the reason. */
51
+ readWorkspaceFile(workspacePath: string | null, relativePath: string): Promise<string>;
52
+ }
53
+
54
+ let services: UiKitServices | null = null;
35
55
 
36
- function usePanelContext(): PanelContextValue {
37
- const value = useContext(PanelContext);
38
- if (!value) throw new Error('Panel hooks must be used inside <PanelProvider>');
39
- return value;
56
+ /** Called ONCE by the host at boot. A plugin must never call this. */
57
+ export function configureUiKit(next: UiKitServices): void {
58
+ services = next;
59
+ }
60
+
61
+ function requireServices(): UiKitServices {
62
+ if (!services) {
63
+ throw new Error(
64
+ 'The OMPChamber UI kit has no host services — this component must be rendered by the OMPChamber app.',
65
+ );
66
+ }
67
+ return services;
40
68
  }
41
69
 
42
70
  /**
43
- * The live panel info.
71
+ * The live panel context, re-rendering on every change.
44
72
  *
45
- * Read through a hook rather than off the context directly so a component
46
- * re-renders when the workspace arrives.
73
+ * `useState` is seeded from the service and updated by its subscription, which
74
+ * is what makes a workspace that arrives after the first paint show up.
47
75
  */
48
- export function usePanelInfo(): PanelInfo {
49
- return usePanelContext().info;
50
- }
76
+ export function usePanelInfo(): PanelContext {
77
+ const [context, setContext] = useState<PanelContext>(() => requireServices().context());
78
+
79
+ useEffect(() => {
80
+ const api = requireServices();
81
+ setContext(api.context());
82
+ return api.subscribe(setContext);
83
+ }, []);
51
84
 
52
- /** The bridge itself, for `api.call(...)`. */
53
- export function usePanelApi(): ChamberPanelApi {
54
- return usePanelContext().api;
85
+ return context;
55
86
  }
56
87
 
57
88
  /** The live palette id. */
@@ -70,42 +101,31 @@ export interface SessionValue {
70
101
  /**
71
102
  * Per-session state, as a `useState` that persists.
72
103
  *
73
- * The value lives in the session store, not in the frame: the frame is destroyed
74
- * whenever the panel is hidden, so local state would be lost on every tab
75
- * switch. Reads once on mount, writes debounced — `input` fires per keystroke,
76
- * and each one would otherwise be a postMessage round trip.
104
+ * The write is debounced: `onInput` fires per keystroke, and each one would
105
+ * otherwise be a store write and a request.
77
106
  */
78
107
  export function useSessionValue(key: string, delayMs = 400): SessionValue {
79
- const api = usePanelApi();
108
+ const { sessionId } = usePanelInfo();
80
109
  const [value, setValue] = useState<string | null>(null);
81
110
  const [status, setStatus] = useState<SessionValue['status']>('loading');
82
111
  const [error, setError] = useState<string | null>(null);
83
112
 
84
113
  useEffect(() => {
85
- let cancelled = false;
86
114
  setStatus('loading');
87
- api.call<string | null>('sessionState.get', { key }).then(
88
- (stored) => {
89
- if (cancelled) return;
90
- setValue(stored ?? '');
91
- setStatus('idle');
92
- },
93
- (cause: unknown) => {
94
- if (cancelled) return;
95
- setError(cause instanceof Error ? cause.message : String(cause));
96
- setStatus('error');
97
- },
98
- );
99
- return () => {
100
- cancelled = true;
101
- };
102
- }, [api, key]);
115
+ try {
116
+ setValue(requireServices().getSessionValue(sessionId, key) ?? '');
117
+ setStatus('idle');
118
+ } catch (cause) {
119
+ setError(cause instanceof Error ? cause.message : String(cause));
120
+ setStatus('error');
121
+ }
122
+ }, [sessionId, key]);
103
123
 
104
124
  useEffect(() => {
105
125
  if (value === null || status !== 'saving') return;
106
- const timer = setTimeout(async () => {
126
+ const timer = setTimeout(() => {
107
127
  try {
108
- await api.call('sessionState.set', { key, value });
128
+ requireServices().setSessionValue(sessionId, key, value);
109
129
  setStatus('saved');
110
130
  } catch (cause) {
111
131
  setError(cause instanceof Error ? cause.message : String(cause));
@@ -113,69 +133,51 @@ export function useSessionValue(key: string, delayMs = 400): SessionValue {
113
133
  }
114
134
  }, delayMs);
115
135
  return () => clearTimeout(timer);
116
- }, [api, key, value, status, delayMs]);
117
-
118
- return {
119
- value,
120
- status,
121
- error,
122
- update: (next: string) => {
123
- setValue(next);
124
- setStatus('saving');
125
- },
126
- };
136
+ }, [sessionId, key, value, status, delayMs]);
137
+
138
+ const update = useCallback((next: string) => {
139
+ setValue(next);
140
+ setStatus('saving');
141
+ }, []);
142
+
143
+ return { value, status, error, update };
127
144
  }
128
145
 
129
- /**
130
- * Mount a panel, rendering `children` once the host has answered.
131
- *
132
- * Renders nothing until `ready` resolves: the panel has no data to draw before
133
- * then, and a half-drawn first frame is worse than none.
134
- */
135
- export function PanelProvider({ children }: { children: ComponentChildren }) {
136
- const [value, setValue] = useState<PanelContextValue | null>(null);
146
+ export interface WorkspaceFile {
147
+ content: string | null;
148
+ loading: boolean;
149
+ error: string | null;
150
+ }
151
+
152
+ /** Read one text file inside the active workspace. */
153
+ export function useWorkspaceFile(relativePath: string): WorkspaceFile {
154
+ const { workspacePath } = usePanelInfo();
155
+ const [content, setContent] = useState<string | null>(null);
156
+ const [loading, setLoading] = useState(true);
137
157
  const [error, setError] = useState<string | null>(null);
138
158
 
139
159
  useEffect(() => {
140
160
  let cancelled = false;
141
- let api: ChamberPanelApi;
142
- try {
143
- api = acquirePanel();
144
- } catch (cause) {
145
- setError(cause instanceof Error ? cause.message : String(cause));
146
- return;
147
- }
148
-
149
- /** Apply a palette id to the document, which is what `:root[data-theme]` keys off. */
150
- const applyTheme = (theme: string) => document.documentElement.setAttribute('data-theme', theme);
151
-
152
- api.ready.then(
153
- (info) => {
154
- if (cancelled) return;
155
- applyTheme(info.theme);
156
- api.onTheme = (theme) => {
157
- applyTheme(theme);
158
- setValue((current) => (current ? { ...current, info: { ...current.info, theme } } : current));
159
- };
160
- api.onContext = (next) => {
161
- if (next.theme) applyTheme(next.theme);
162
- setValue((current) => (current ? { ...current, info: { ...current.info, ...next } } : current));
163
- };
164
- setValue({ api, info });
165
- },
166
- (cause: unknown) => {
167
- if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause));
168
- },
169
- );
170
-
161
+ setLoading(true);
162
+ setError(null);
163
+ requireServices()
164
+ .readWorkspaceFile(workspacePath, relativePath)
165
+ .then(
166
+ (text) => {
167
+ if (cancelled) return;
168
+ setContent(text);
169
+ setLoading(false);
170
+ },
171
+ (cause: unknown) => {
172
+ if (cancelled) return;
173
+ setError(cause instanceof Error ? cause.message : String(cause));
174
+ setLoading(false);
175
+ },
176
+ );
171
177
  return () => {
172
178
  cancelled = true;
173
- api.onTheme = undefined;
174
- api.onContext = undefined;
175
179
  };
176
- }, []);
180
+ }, [workspacePath, relativePath]);
177
181
 
178
- if (error) return <p class="oc-note oc-note-error">Could not attach to the chamber: {error}</p>;
179
- if (!value) return null;
180
- return <PanelContext.Provider value={value}>{children}</PanelContext.Provider>;
182
+ return { content, loading, error };
181
183
  }
package/src/styles.css DELETED
@@ -1,187 +0,0 @@
1
- /**
2
- * The UI kit's stylesheet.
3
- *
4
- * The frame has an opaque origin, so it cannot reach the chamber's stylesheet —
5
- * the palette arrives as `data-theme` on `<html>` (set by `PanelProvider`) and
6
- * this file maps it. That is the whole reason a kit ships CSS: without it, every
7
- * panel would invent its own colours and drift from the app.
8
- *
9
- * The palette ids are the two the chamber ships most often. A plugin may add a
10
- * block for a theme it wants to support; an unknown id leaves the defaults,
11
- * which is a readable fallback rather than a broken one.
12
- */
13
-
14
- :root {
15
- --oc-bg: #faf8f3;
16
- --oc-paper: #ffffff;
17
- --oc-ink: #141310;
18
- --oc-muted: rgba(20, 19, 16, 0.55);
19
- --oc-faint: rgba(20, 19, 16, 0.38);
20
- --oc-line: rgba(20, 19, 16, 0.14);
21
- --oc-error: #b3261e;
22
- }
23
-
24
- :root[data-theme='one-dark-pro-soft'] {
25
- --oc-bg: #21252b;
26
- --oc-paper: #282c34;
27
- --oc-ink: #d7dae0;
28
- --oc-muted: rgba(215, 218, 224, 0.6);
29
- --oc-faint: rgba(215, 218, 224, 0.42);
30
- --oc-line: rgba(215, 218, 224, 0.16);
31
- --oc-error: #e06c75;
32
- }
33
-
34
- html,
35
- body {
36
- margin: 0;
37
- height: 100%;
38
- }
39
-
40
- body {
41
- background: var(--oc-bg);
42
- color: var(--oc-ink);
43
- font-family: system-ui, -apple-system, sans-serif;
44
- font-size: 13px;
45
- }
46
-
47
- /* Panel chrome ------------------------------------------------------------ */
48
-
49
- .oc-panel {
50
- display: flex;
51
- flex-direction: column;
52
- height: 100%;
53
- }
54
-
55
- .oc-panel-header {
56
- padding: 12px 14px;
57
- border-bottom: 1px solid var(--oc-line);
58
- flex-shrink: 0;
59
- }
60
-
61
- .oc-panel-title {
62
- margin: 0;
63
- font-size: 13px;
64
- font-weight: 600;
65
- }
66
-
67
- .oc-panel-body {
68
- flex: 1;
69
- min-height: 0;
70
- display: flex;
71
- flex-direction: column;
72
- gap: 12px;
73
- padding: 12px 14px 14px;
74
- overflow: auto;
75
- }
76
-
77
- /* Fields ------------------------------------------------------------------ */
78
-
79
- .oc-fields {
80
- display: flex;
81
- flex-direction: column;
82
- gap: 8px;
83
- }
84
-
85
- .oc-field {
86
- display: flex;
87
- flex-direction: column;
88
- gap: 2px;
89
- }
90
-
91
- .oc-label {
92
- font-size: 10px;
93
- text-transform: uppercase;
94
- letter-spacing: 0.06em;
95
- color: var(--oc-muted);
96
- font-family: ui-monospace, monospace;
97
- }
98
-
99
- .oc-value {
100
- font-family: ui-monospace, monospace;
101
- font-size: 12px;
102
- word-break: break-all;
103
- }
104
-
105
- .oc-hint {
106
- font-size: 11px;
107
- color: var(--oc-muted);
108
- }
109
-
110
- /* Text field -------------------------------------------------------------- */
111
-
112
- .oc-textarea-field {
113
- flex: 1;
114
- min-height: 0;
115
- display: flex;
116
- flex-direction: column;
117
- gap: 6px;
118
- }
119
-
120
- .oc-textarea {
121
- flex: 1;
122
- min-height: 80px;
123
- resize: none;
124
- border: 1px solid var(--oc-line);
125
- border-radius: 6px;
126
- padding: 8px;
127
- font: 12px/1.6 ui-monospace, monospace;
128
- background: transparent;
129
- color: inherit;
130
- outline: none;
131
- }
132
-
133
- .oc-textarea:focus {
134
- border-color: var(--oc-ink);
135
- }
136
-
137
- .oc-textarea:disabled {
138
- opacity: 0.5;
139
- }
140
-
141
- /* Button ------------------------------------------------------------------ */
142
-
143
- .oc-button {
144
- font: inherit;
145
- font-size: 12px;
146
- padding: 5px 10px;
147
- border-radius: 6px;
148
- border: 1px solid var(--oc-line);
149
- background: transparent;
150
- color: inherit;
151
- cursor: pointer;
152
- }
153
-
154
- .oc-button:hover:not(:disabled) {
155
- background: color-mix(in srgb, var(--oc-ink) 6%, transparent);
156
- }
157
-
158
- .oc-button:disabled {
159
- opacity: 0.45;
160
- cursor: default;
161
- }
162
-
163
- .oc-button-primary {
164
- background: var(--oc-ink);
165
- color: var(--oc-bg);
166
- border-color: var(--oc-ink);
167
- }
168
-
169
- /* Messages ---------------------------------------------------------------- */
170
-
171
- .oc-empty {
172
- margin: 0;
173
- padding: 24px 8px;
174
- text-align: center;
175
- font-size: 12px;
176
- color: var(--oc-faint);
177
- }
178
-
179
- .oc-note {
180
- margin: 0;
181
- font-size: 11px;
182
- color: var(--oc-muted);
183
- }
184
-
185
- .oc-note-error {
186
- color: var(--oc-error);
187
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * Type declaration for `@ompchamber/ui/styles.css`.
3
- *
4
- * TypeScript will not accept a side-effect import of a `.css` file without a
5
- * declaration beside it, and the error (`TS2882: Cannot find module or type
6
- * declarations for side-effect import`) is the first thing a plugin author would
7
- * hit. This is the shape TypeScript looks for — `styles.css` resolves to
8
- * `styles.d.css.ts` — and it requires `allowArbitraryExtensions: true` in the
9
- * consumer's tsconfig, which the plugin tsconfig in the docs sets.
10
- *
11
- * It is exported rather than ambient so the file is a module, which is what
12
- * `allowArbitraryExtensions` expects; the stylesheet itself is the payload, and
13
- * nothing is imported from here at runtime.
14
- */
15
-
16
- export {};