@deepseek-insight/container-base 0.0.0-stage → 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/README.md +77 -2
- package/dist/components/ChordPaletteOverlay.svelte +99 -0
- package/dist/components/ChordPaletteOverlay.svelte.d.ts +25 -0
- package/dist/components/PaneBase.svelte +159 -0
- package/dist/components/PaneBase.svelte.d.ts +27 -0
- package/dist/components/PanelHeaderSkeleton.svelte +85 -0
- package/dist/components/PanelHeaderSkeleton.svelte.d.ts +19 -0
- package/dist/components/PanelsZoom.svelte +213 -0
- package/dist/components/PanelsZoom.svelte.d.ts +26 -0
- package/dist/components/RailColumn.svelte +91 -0
- package/dist/components/RailColumn.svelte.d.ts +15 -0
- package/dist/components/ResizeGutter.svelte +68 -0
- package/dist/components/ResizeGutter.svelte.d.ts +13 -0
- package/dist/components/SidebarContainer.svelte +174 -0
- package/dist/components/SidebarContainer.svelte.d.ts +42 -0
- package/dist/components/SliderWidth.svelte +80 -0
- package/dist/components/SliderWidth.svelte.d.ts +27 -0
- package/dist/components/SliderZoom.svelte +77 -0
- package/dist/components/SliderZoom.svelte.d.ts +25 -0
- package/dist/i18n/LanguageMenu.svelte +165 -0
- package/dist/i18n/LanguageMenu.svelte.d.ts +25 -0
- package/dist/i18n/locale-prefs.svelte.d.ts +24 -0
- package/dist/i18n/locale-prefs.svelte.js +85 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +35 -0
- package/dist/interaction/.gitkeep +0 -0
- package/dist/interaction/chord-machine.d.ts +108 -0
- package/dist/interaction/chord-machine.js +121 -0
- package/dist/interaction/gutter-resize.d.ts +37 -0
- package/dist/interaction/gutter-resize.js +41 -0
- package/dist/layout/.gitkeep +0 -0
- package/dist/layout/floor-math.d.ts +48 -0
- package/dist/layout/floor-math.js +60 -0
- package/dist/layout/placement.d.ts +18 -0
- package/dist/layout/placement.js +27 -0
- package/dist/registry/.gitkeep +0 -0
- package/dist/registry/action-registry.d.ts +30 -0
- package/dist/registry/action-registry.js +44 -0
- package/dist/storage/create-prefs-store.d.ts +25 -0
- package/dist/storage/create-prefs-store.js +67 -0
- package/dist/storage/namespace.d.ts +27 -0
- package/dist/storage/namespace.js +32 -0
- package/dist/theme/.gitkeep +0 -0
- package/dist/theme/tokens.css +35 -0
- package/dist/theming/ThemeToggle.svelte +170 -0
- package/dist/theming/ThemeToggle.svelte.d.ts +27 -0
- package/dist/theming/applier.d.ts +15 -0
- package/dist/theming/applier.js +29 -0
- package/dist/theming/theme-prefs.svelte.d.ts +21 -0
- package/dist/theming/theme-prefs.svelte.js +91 -0
- package/dist/types/.gitkeep +0 -0
- package/package.json +34 -5
- package/src/components/ChordPaletteOverlay.svelte +99 -0
- package/src/components/PaneBase.svelte +159 -0
- package/src/components/PanelHeaderSkeleton.svelte +85 -0
- package/src/components/PanelsZoom.svelte +213 -0
- package/src/components/RailColumn.svelte +91 -0
- package/src/components/ResizeGutter.svelte +68 -0
- package/src/components/SidebarContainer.svelte +174 -0
- package/src/components/SliderWidth.svelte +80 -0
- package/src/components/SliderZoom.svelte +77 -0
- package/src/i18n/LanguageMenu.svelte +165 -0
- package/src/i18n/locale-prefs.svelte.ts +112 -0
- package/src/index.ts +67 -0
- package/src/interaction/.gitkeep +0 -0
- package/src/interaction/chord-machine.ts +208 -0
- package/src/interaction/gutter-resize.ts +76 -0
- package/src/layout/.gitkeep +0 -0
- package/src/layout/floor-math.ts +94 -0
- package/src/layout/placement.ts +30 -0
- package/src/registry/.gitkeep +0 -0
- package/src/registry/action-registry.ts +62 -0
- package/src/storage/create-prefs-store.ts +87 -0
- package/src/storage/namespace.ts +34 -0
- package/src/theme/.gitkeep +0 -0
- package/src/theme/tokens.css +35 -0
- package/src/theming/ThemeToggle.svelte +170 -0
- package/src/theming/applier.ts +30 -0
- package/src/theming/theme-prefs.svelte.ts +118 -0
- package/src/types/.gitkeep +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage namespace — the ONE home of the workspace-profile key convention
|
|
3
|
+
* (generalized from DSI's storage-profile.ts; ADR-0062 D4). Consumer keys
|
|
4
|
+
* keep their exact existing strings: the suffix rule is the only thing
|
|
5
|
+
* this module owns.
|
|
6
|
+
*
|
|
7
|
+
* no profile → my-key
|
|
8
|
+
* ?profile=widi → my-key_widi
|
|
9
|
+
*
|
|
10
|
+
* The profile is sticky and sanitized here once — every prefs store routes
|
|
11
|
+
* its key through the namespace fn so no two call sites re-implement it.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize a raw `?profile=` value into a storage-key-safe segment.
|
|
15
|
+
* Keeps `[A-Za-z0-9_-]` (case preserved — profiles are exact ids), caps at
|
|
16
|
+
* 40 chars; anything that sanitizes to nothing is not a profile.
|
|
17
|
+
* @returns the safe segment, or null when the value carries no usable
|
|
18
|
+
* characters (the caller then behaves as if no profile were set).
|
|
19
|
+
*/
|
|
20
|
+
export declare function sanitizeWorkspaceProfile(raw: string): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The storage key for a workspace profile.
|
|
23
|
+
* @param base the un-suffixed storage key (e.g. 'dsi-panels').
|
|
24
|
+
* @param profile sanitized profile segment, or null/undefined for the
|
|
25
|
+
* default desk (the base key unchanged).
|
|
26
|
+
*/
|
|
27
|
+
export declare function storageKey(base: string, profile?: string | null): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage namespace — the ONE home of the workspace-profile key convention
|
|
3
|
+
* (generalized from DSI's storage-profile.ts; ADR-0062 D4). Consumer keys
|
|
4
|
+
* keep their exact existing strings: the suffix rule is the only thing
|
|
5
|
+
* this module owns.
|
|
6
|
+
*
|
|
7
|
+
* no profile → my-key
|
|
8
|
+
* ?profile=widi → my-key_widi
|
|
9
|
+
*
|
|
10
|
+
* The profile is sticky and sanitized here once — every prefs store routes
|
|
11
|
+
* its key through the namespace fn so no two call sites re-implement it.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize a raw `?profile=` value into a storage-key-safe segment.
|
|
15
|
+
* Keeps `[A-Za-z0-9_-]` (case preserved — profiles are exact ids), caps at
|
|
16
|
+
* 40 chars; anything that sanitizes to nothing is not a profile.
|
|
17
|
+
* @returns the safe segment, or null when the value carries no usable
|
|
18
|
+
* characters (the caller then behaves as if no profile were set).
|
|
19
|
+
*/
|
|
20
|
+
export function sanitizeWorkspaceProfile(raw) {
|
|
21
|
+
const safe = raw.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 40);
|
|
22
|
+
return safe.length > 0 ? safe : null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The storage key for a workspace profile.
|
|
26
|
+
* @param base the un-suffixed storage key (e.g. 'dsi-panels').
|
|
27
|
+
* @param profile sanitized profile segment, or null/undefined for the
|
|
28
|
+
* default desk (the base key unchanged).
|
|
29
|
+
*/
|
|
30
|
+
export function storageKey(base, profile) {
|
|
31
|
+
return profile ? `${base}_${profile}` : base;
|
|
32
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @deepseek-insight/container-base — default theme tokens (ADR-0062 W5,
|
|
3
|
+
* D7): a fresh consumer is styled out of the box; the host OVERRIDES every
|
|
4
|
+
* var (DSI's theme defines all of them, byte-identical visuals).
|
|
5
|
+
*
|
|
6
|
+
* --zoom is NOT a theme token: it is a per-frame runtime variable the
|
|
7
|
+
* PanelsZoom component writes itself.
|
|
8
|
+
*
|
|
9
|
+
* Ground rule (dev/article/2026-11/04.re-architect, 2026-11-14): styling
|
|
10
|
+
* crosses ONLY through tokens — components never carry a skin. W6 added
|
|
11
|
+
* the chord-palette tokens after the boundary audit found the overlay
|
|
12
|
+
* wearing Tailwind palette classes.
|
|
13
|
+
*/
|
|
14
|
+
:root {
|
|
15
|
+
/* Surfaces */
|
|
16
|
+
--color-surface-primary: #ffffff;
|
|
17
|
+
--color-surface-secondary: #f1f3f5;
|
|
18
|
+
--color-surface-elevated: #ffffff;
|
|
19
|
+
--color-surface-border: #dee2e6;
|
|
20
|
+
--color-header-wash: #fff8ec;
|
|
21
|
+
|
|
22
|
+
/* Text */
|
|
23
|
+
--color-text-primary: #212529;
|
|
24
|
+
--color-text-secondary: #6c757d;
|
|
25
|
+
|
|
26
|
+
/* Accents (the selection ring gradient stops) */
|
|
27
|
+
--color-accent-blue: #3b82f6;
|
|
28
|
+
--color-accent-purple: #a855f7;
|
|
29
|
+
--color-accent-rose: #f43f5e;
|
|
30
|
+
|
|
31
|
+
/* Chord palette (the armed overlay's decay ticker — W6 ground rule:
|
|
32
|
+
* theming crosses only through tokens; the component ships no skin) */
|
|
33
|
+
--color-chord-armed: #86efac;
|
|
34
|
+
--color-chord-idle: #fef08a;
|
|
35
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ThemeToggle — the generic three-state lamp mount (ADR-0063 D2).
|
|
4
|
+
* The MECHANISM lives in theming/theme-prefs.svelte.ts; this component
|
|
5
|
+
* carries NO words and NO skin: every label comes in as a prop, every
|
|
6
|
+
* icon as a snippet, every color as a token var (boundary suite).
|
|
7
|
+
* Structure follows the DSI footer lamp: one trigger whose icon is the
|
|
8
|
+
* current choice, a popup with the three states, check on the active
|
|
9
|
+
* one, click-outside closes, dropUp for rail-bottom mounts.
|
|
10
|
+
*/
|
|
11
|
+
import type { Component, Snippet } from 'svelte';
|
|
12
|
+
|
|
13
|
+
export type ThemeChoice = 'system' | 'light' | 'dark';
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
/** The consumer's ThemePrefs instance (createThemePrefs). */
|
|
17
|
+
prefs,
|
|
18
|
+
/** aria-label / title for the trigger (consumer's words). */
|
|
19
|
+
triggerLabel,
|
|
20
|
+
/** Per-state words (consumer's dictionary). */
|
|
21
|
+
optionLabels,
|
|
22
|
+
/** Per-state icon components (consumer's icon set). */
|
|
23
|
+
icons,
|
|
24
|
+
/** Open the list upward (rail-bottom mounts). */
|
|
25
|
+
dropUp = false
|
|
26
|
+
}: {
|
|
27
|
+
prefs: {
|
|
28
|
+
readonly choice: ThemeChoice;
|
|
29
|
+
set: (c: ThemeChoice) => void;
|
|
30
|
+
};
|
|
31
|
+
triggerLabel: string;
|
|
32
|
+
optionLabels: Record<ThemeChoice, string>;
|
|
33
|
+
icons: Record<ThemeChoice, Component<{ size?: number; 'aria-hidden'?: boolean | 'true' | 'false' }>>;
|
|
34
|
+
dropUp?: boolean;
|
|
35
|
+
} = $props();
|
|
36
|
+
|
|
37
|
+
let open = $state(false);
|
|
38
|
+
let rootEl = $state<HTMLElement | null>(null);
|
|
39
|
+
|
|
40
|
+
const CHOICES: ThemeChoice[] = ['system', 'light', 'dark'];
|
|
41
|
+
|
|
42
|
+
const TriggerIcon = $derived(icons[prefs.choice]);
|
|
43
|
+
|
|
44
|
+
function choose(c: ThemeChoice): void {
|
|
45
|
+
open = false;
|
|
46
|
+
prefs.set(c);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// outside click closes — one window listener while open (removed on close)
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (!open) return;
|
|
52
|
+
const onDocClick = (ev: MouseEvent) => {
|
|
53
|
+
if (rootEl && !rootEl.contains(ev.target as Node)) open = false;
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener('click', onDocClick);
|
|
56
|
+
return () => document.removeEventListener('click', onDocClick);
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
class="cb-theme-toggle"
|
|
62
|
+
data-testid="cb-theme-toggle"
|
|
63
|
+
bind:this={rootEl}
|
|
64
|
+
>
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
class="cb-theme-toggle-trigger"
|
|
68
|
+
title={triggerLabel}
|
|
69
|
+
aria-label={triggerLabel}
|
|
70
|
+
aria-expanded={open}
|
|
71
|
+
data-testid="cb-theme-toggle-trigger"
|
|
72
|
+
onclick={() => (open = !open)}
|
|
73
|
+
>
|
|
74
|
+
<TriggerIcon size={16} />
|
|
75
|
+
</button>
|
|
76
|
+
{#if open}
|
|
77
|
+
<div
|
|
78
|
+
class="cb-theme-toggle-list"
|
|
79
|
+
class:cb-drop-up={dropUp}
|
|
80
|
+
data-testid="cb-theme-toggle-list"
|
|
81
|
+
role="menu"
|
|
82
|
+
>
|
|
83
|
+
{#each CHOICES as c (c)}
|
|
84
|
+
{@const Icon = icons[c]}
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
class="cb-theme-option"
|
|
88
|
+
class:cb-option-active={prefs.choice === c}
|
|
89
|
+
data-testid={'cb-theme-option-' + c}
|
|
90
|
+
data-active={prefs.choice === c ? 'true' : 'false'}
|
|
91
|
+
role="menuitem"
|
|
92
|
+
onclick={() => choose(c)}
|
|
93
|
+
>
|
|
94
|
+
<span class="cb-theme-option-row">
|
|
95
|
+
<Icon size={13} aria-hidden="true" />
|
|
96
|
+
{optionLabels[c]}
|
|
97
|
+
</span>
|
|
98
|
+
{#if prefs.choice === c}<span aria-hidden="true">✓</span>{/if}
|
|
99
|
+
</button>
|
|
100
|
+
{/each}
|
|
101
|
+
</div>
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
/* Structure only — every color crosses through tokens (ground rule).
|
|
107
|
+
* The outside-click close effect lives with the consumer host for now
|
|
108
|
+
* (see tests); the component itself holds no globals. */
|
|
109
|
+
.cb-theme-toggle {
|
|
110
|
+
position: relative;
|
|
111
|
+
display: inline-block;
|
|
112
|
+
}
|
|
113
|
+
.cb-theme-toggle-trigger {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 0.25rem;
|
|
117
|
+
border: none;
|
|
118
|
+
background: transparent;
|
|
119
|
+
color: var(--color-text-secondary);
|
|
120
|
+
border-radius: 0.25rem;
|
|
121
|
+
padding: 0.25rem;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
}
|
|
124
|
+
.cb-theme-toggle-trigger:hover {
|
|
125
|
+
color: var(--color-text-primary);
|
|
126
|
+
}
|
|
127
|
+
.cb-theme-toggle-list {
|
|
128
|
+
position: absolute;
|
|
129
|
+
right: 0;
|
|
130
|
+
z-index: 50;
|
|
131
|
+
width: 9rem;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
margin-top: 0.25rem;
|
|
134
|
+
border-radius: 0.375rem;
|
|
135
|
+
border: 1px solid var(--color-surface-border);
|
|
136
|
+
background: var(--color-surface-elevated);
|
|
137
|
+
color: var(--color-text-primary);
|
|
138
|
+
box-shadow: 0 4px 6px -1px var(--color-surface-border);
|
|
139
|
+
}
|
|
140
|
+
.cb-theme-toggle-list.cb-drop-up {
|
|
141
|
+
bottom: 100%;
|
|
142
|
+
margin-top: 0;
|
|
143
|
+
margin-bottom: 0.25rem;
|
|
144
|
+
}
|
|
145
|
+
.cb-theme-option {
|
|
146
|
+
display: flex;
|
|
147
|
+
width: 100%;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
padding: 0.375rem 0.75rem;
|
|
151
|
+
text-align: left;
|
|
152
|
+
font-size: 0.75rem;
|
|
153
|
+
border: none;
|
|
154
|
+
background: transparent;
|
|
155
|
+
color: inherit;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
}
|
|
158
|
+
.cb-theme-option:hover {
|
|
159
|
+
background: var(--color-surface-secondary);
|
|
160
|
+
}
|
|
161
|
+
.cb-theme-option.cb-option-active {
|
|
162
|
+
color: var(--color-accent-purple);
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
}
|
|
165
|
+
.cb-theme-option-row {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 0.375rem;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeToggle — the generic three-state lamp mount (ADR-0063 D2).
|
|
3
|
+
* The MECHANISM lives in theming/theme-prefs.svelte.ts; this component
|
|
4
|
+
* carries NO words and NO skin: every label comes in as a prop, every
|
|
5
|
+
* icon as a snippet, every color as a token var (boundary suite).
|
|
6
|
+
* Structure follows the DSI footer lamp: one trigger whose icon is the
|
|
7
|
+
* current choice, a popup with the three states, check on the active
|
|
8
|
+
* one, click-outside closes, dropUp for rail-bottom mounts.
|
|
9
|
+
*/
|
|
10
|
+
import type { Component } from 'svelte';
|
|
11
|
+
export type ThemeChoice = 'system' | 'light' | 'dark';
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
prefs: {
|
|
14
|
+
readonly choice: ThemeChoice;
|
|
15
|
+
set: (c: ThemeChoice) => void;
|
|
16
|
+
};
|
|
17
|
+
triggerLabel: string;
|
|
18
|
+
optionLabels: Record<ThemeChoice, string>;
|
|
19
|
+
icons: Record<ThemeChoice, Component<{
|
|
20
|
+
size?: number;
|
|
21
|
+
'aria-hidden'?: boolean | 'true' | 'false';
|
|
22
|
+
}>>;
|
|
23
|
+
dropUp?: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare const ThemeToggle: Component<$$ComponentProps, {}, "">;
|
|
26
|
+
type ThemeToggle = ReturnType<typeof ThemeToggle>;
|
|
27
|
+
export default ThemeToggle;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-paint theme applier — an inline SCRIPT SOURCE STRING the consumer
|
|
3
|
+
* embeds in its HTML template (ADR-0063 D2). It re-derives the storage key
|
|
4
|
+
* with the SAME grammar as theming/theme-prefs.svelte.ts (namespace.ts:
|
|
5
|
+
* <baseKey> + '_' + sanitized ?profile=), reads the choice, resolves system
|
|
6
|
+
* against the OS, and sets html[data-theme] BEFORE first paint — no flash.
|
|
7
|
+
*
|
|
8
|
+
* It cannot import modules (it runs before the bundle), so the derivation is
|
|
9
|
+
* generated from this one function: consumers never hand-copy it.
|
|
10
|
+
*
|
|
11
|
+
* Pure string generation — no runes — so plain node (strip-types) can import
|
|
12
|
+
* it for HTML template generation (scripts/sync-theme-applier.mjs).
|
|
13
|
+
*/
|
|
14
|
+
/** Generate the inline script source for a consumer's theme base key. */
|
|
15
|
+
export declare function themeApplierScript(baseKey: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-paint theme applier — an inline SCRIPT SOURCE STRING the consumer
|
|
3
|
+
* embeds in its HTML template (ADR-0063 D2). It re-derives the storage key
|
|
4
|
+
* with the SAME grammar as theming/theme-prefs.svelte.ts (namespace.ts:
|
|
5
|
+
* <baseKey> + '_' + sanitized ?profile=), reads the choice, resolves system
|
|
6
|
+
* against the OS, and sets html[data-theme] BEFORE first paint — no flash.
|
|
7
|
+
*
|
|
8
|
+
* It cannot import modules (it runs before the bundle), so the derivation is
|
|
9
|
+
* generated from this one function: consumers never hand-copy it.
|
|
10
|
+
*
|
|
11
|
+
* Pure string generation — no runes — so plain node (strip-types) can import
|
|
12
|
+
* it for HTML template generation (scripts/sync-theme-applier.mjs).
|
|
13
|
+
*/
|
|
14
|
+
/** Generate the inline script source for a consumer's theme base key. */
|
|
15
|
+
export function themeApplierScript(baseKey) {
|
|
16
|
+
return `try {
|
|
17
|
+
var m = location.search.match(/[?&]profile=([^&]*)/);
|
|
18
|
+
var prof = m ? m[1].replace(/[^A-Za-z0-9_-]/g, '').slice(0, 40) : '';
|
|
19
|
+
var k = '${baseKey}' + (prof ? '_' + prof : '');
|
|
20
|
+
var t = localStorage.getItem(k);
|
|
21
|
+
if (t !== 'light' && t !== 'dark' && t !== 'system') t = 'system';
|
|
22
|
+
document.documentElement.dataset.theme =
|
|
23
|
+
t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches)
|
|
24
|
+
? 'dark'
|
|
25
|
+
: 'light';
|
|
26
|
+
} catch (e) {
|
|
27
|
+
document.documentElement.dataset.theme = 'light';
|
|
28
|
+
}`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ThemeChoice = 'system' | 'light' | 'dark';
|
|
2
|
+
export type ResolvedTheme = 'light' | 'dark';
|
|
3
|
+
export interface ThemePrefsOptions {
|
|
4
|
+
/** The un-suffixed localStorage key (e.g. 'dsi-theme', 'pg-theme'). */
|
|
5
|
+
baseKey: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ThemePrefs {
|
|
8
|
+
readonly choice: ThemeChoice;
|
|
9
|
+
readonly resolved: ResolvedTheme;
|
|
10
|
+
/** Mount-time adoption: the pre-paint applier already resolved and set
|
|
11
|
+
* the attribute before the bundle ran — ADOPT it (flash-free),
|
|
12
|
+
* re-deriving the choice from storage. Paints only when the attribute
|
|
13
|
+
* was somehow missing. */
|
|
14
|
+
adopt(): void;
|
|
15
|
+
/** Persist the CHOICE (never the resolved value — system keeps following
|
|
16
|
+
* the OS), then paint. */
|
|
17
|
+
set(next: ThemeChoice): void;
|
|
18
|
+
/** Live OS following, only while the choice is system. */
|
|
19
|
+
listen(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createThemePrefs(options: ThemePrefsOptions): ThemePrefs;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The base Three-State Lamp — the switch MECHANISM (ADR-0063 D2, moved out
|
|
2
|
+
// of DSI's hand-rolled twin). Owns a three-state choice (system / light /
|
|
3
|
+
// dark), persists it under localStorage[<baseKey>], resolves it against the
|
|
4
|
+
// OS, and sets html[data-theme] — the single hinge the token layer reads.
|
|
5
|
+
// The ONLY other place that derives the storage key is the inline pre-paint
|
|
6
|
+
// applier (theming/applier.ts) the consumer inlines in its HTML template;
|
|
7
|
+
// key grammar flows through storage/namespace so the two cannot drift.
|
|
8
|
+
// Key grammar: EVERY <baseKey> is profile-scoped (?profile= param), so each
|
|
9
|
+
// desk remembers its own lamp.
|
|
10
|
+
import { storageKey, sanitizeWorkspaceProfile } from '../storage/namespace';
|
|
11
|
+
export function createThemePrefs(options) {
|
|
12
|
+
const baseKey = options.baseKey;
|
|
13
|
+
/** The active workspace profile — the sticky ?profile= param, sanitized
|
|
14
|
+
* once here (mirror of the pre-paint derivation in the applier). */
|
|
15
|
+
const currentProfile = () => typeof location === 'undefined'
|
|
16
|
+
? null
|
|
17
|
+
: sanitizeWorkspaceProfile(new URLSearchParams(location.search).get('profile') ?? '');
|
|
18
|
+
const storageKeyFor = () => storageKey(baseKey, currentProfile());
|
|
19
|
+
const osPrefersDark = () => typeof window !== 'undefined' &&
|
|
20
|
+
!!window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
21
|
+
/** Defensive read — SSR-safe, junk -> system. */
|
|
22
|
+
const readStoredChoice = () => {
|
|
23
|
+
if (typeof localStorage === 'undefined')
|
|
24
|
+
return 'system';
|
|
25
|
+
try {
|
|
26
|
+
const raw = localStorage.getItem(storageKeyFor());
|
|
27
|
+
return raw === 'light' || raw === 'dark' || raw === 'system' ? raw : 'system';
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return 'system';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const resolve = (choice) => choice === 'system' ? (osPrefersDark() ? 'dark' : 'light') : choice;
|
|
34
|
+
let choice = $state('system');
|
|
35
|
+
let resolved = $state('light');
|
|
36
|
+
let listeningMq = null;
|
|
37
|
+
const apply = () => {
|
|
38
|
+
if (typeof document !== 'undefined')
|
|
39
|
+
document.documentElement.dataset.theme = resolved;
|
|
40
|
+
};
|
|
41
|
+
const persist = () => {
|
|
42
|
+
if (typeof localStorage === 'undefined')
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
localStorage.setItem(storageKeyFor(), choice);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* blocked storage — the attribute still flips for this session */
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
get choice() {
|
|
53
|
+
return choice;
|
|
54
|
+
},
|
|
55
|
+
get resolved() {
|
|
56
|
+
return resolved;
|
|
57
|
+
},
|
|
58
|
+
adopt() {
|
|
59
|
+
choice = readStoredChoice();
|
|
60
|
+
const attr = typeof document !== 'undefined' ? document.documentElement.dataset.theme : undefined;
|
|
61
|
+
if (attr === 'dark' || attr === 'light') {
|
|
62
|
+
resolved = attr;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
resolved = resolve(choice);
|
|
66
|
+
apply();
|
|
67
|
+
}
|
|
68
|
+
this.listen();
|
|
69
|
+
},
|
|
70
|
+
set(next) {
|
|
71
|
+
choice = next;
|
|
72
|
+
resolved = resolve(next);
|
|
73
|
+
persist();
|
|
74
|
+
apply();
|
|
75
|
+
},
|
|
76
|
+
listen() {
|
|
77
|
+
if (typeof window === 'undefined' || !window.matchMedia)
|
|
78
|
+
return;
|
|
79
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
80
|
+
if (listeningMq === mq)
|
|
81
|
+
return; // one listener per media-query object
|
|
82
|
+
listeningMq = mq;
|
|
83
|
+
mq.addEventListener('change', (e) => {
|
|
84
|
+
if (choice !== 'system')
|
|
85
|
+
return; // pinned ignores the OS
|
|
86
|
+
resolved = e.matches ? 'dark' : 'light';
|
|
87
|
+
apply();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@deepseek-insight/container-base",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"svelte": "./dist/index.js",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./theme/tokens.css": "./src/theme/tokens.css"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"svelte": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependenciesMeta": {
|
|
22
|
+
"svelte": {
|
|
23
|
+
"required": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build:package": "svelte-package -i src -o dist",
|
|
28
|
+
"lint:package": "publint",
|
|
29
|
+
"test": "vitest run --root ../.. --project dom packages/container-base/tests"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@sveltejs/package": "^3.0.2",
|
|
33
|
+
"publint": "^0.3.25"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ChordPaletteOverlay — the armed chord's face (ADR-0006a amendment
|
|
4
|
+
* D16/D18 port; container-base W4, ADR-0062 D8): a centered overlay
|
|
5
|
+
* with the live option rows and a decay ticker. The consumer passes
|
|
6
|
+
* its OWN labels (rows carry label text; title/ariaLabel arrive as
|
|
7
|
+
* props — the package never learns a translation system) and its
|
|
8
|
+
* action-window length (windowMs). The overlay holds NO armed state:
|
|
9
|
+
* open/since come in, onexpire fires out.
|
|
10
|
+
*
|
|
11
|
+
* While open it fills a footer progress bar 0→100% across
|
|
12
|
+
* idleCloseMs — inside windowMs the bar reads in-action, past it
|
|
13
|
+
* idle — and at 100% fires onexpire once. The clock is THIS
|
|
14
|
+
* component's visibility lifecycle (D18: view-lifecycle, not
|
|
15
|
+
* reducer state).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
open,
|
|
20
|
+
rows,
|
|
21
|
+
since,
|
|
22
|
+
windowMs,
|
|
23
|
+
idleCloseMs = 10_000,
|
|
24
|
+
title,
|
|
25
|
+
ariaLabel,
|
|
26
|
+
onexpire
|
|
27
|
+
}: {
|
|
28
|
+
/** True between the arming chord and the fire / disarm / decay. */
|
|
29
|
+
open: boolean;
|
|
30
|
+
/** The live options, already filtered by the consumer's reducer. */
|
|
31
|
+
rows: { key: string; label: string }[];
|
|
32
|
+
/** Date.now() of the arming — the decay clock's zero. */
|
|
33
|
+
since: number;
|
|
34
|
+
/** The consumer's action-window length (ms) — inside it the bar
|
|
35
|
+
* reads in-action; the consumer owns the semantics (ADR-0052). */
|
|
36
|
+
windowMs: number;
|
|
37
|
+
/** The idle window before the overlay closes itself (default 10s). */
|
|
38
|
+
idleCloseMs?: number;
|
|
39
|
+
/** The strip title (consumer-localized, names the armed prefix). */
|
|
40
|
+
title: string;
|
|
41
|
+
/** Accessible name for the overlay (consumer-localized). */
|
|
42
|
+
ariaLabel: string;
|
|
43
|
+
/** Fired once at 100% — the host resets the chord. */
|
|
44
|
+
onexpire?: () => void;
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
// The decay clock: the ticker runs ONLY while open — the host's timer
|
|
48
|
+
// discipline counts every persistent client interval, and an overlay
|
|
49
|
+
// that lives for seconds must not add a permanent one.
|
|
50
|
+
let now = $state(Date.now());
|
|
51
|
+
const elapsed = $derived(open ? Math.max(0, Math.min(now - (since || Date.now()), idleCloseMs)) : 0);
|
|
52
|
+
const expired = $derived(open && elapsed >= idleCloseMs);
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (!open) return;
|
|
56
|
+
const ticker = setInterval(() => (now = Date.now()), 100);
|
|
57
|
+
return () => clearInterval(ticker);
|
|
58
|
+
});
|
|
59
|
+
$effect(() => {
|
|
60
|
+
if (expired) onexpire?.();
|
|
61
|
+
});
|
|
62
|
+
const pct = $derived(Math.round((elapsed / idleCloseMs) * 100));
|
|
63
|
+
const inActionWindow = $derived(elapsed < windowMs);
|
|
64
|
+
// Ground rule: the ticker colors cross through tokens — no palette classes (W6).
|
|
65
|
+
const barColor = $derived(inActionWindow ? 'bg-[var(--color-chord-armed)]' : 'bg-[var(--color-chord-idle)]');
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
{#if open}
|
|
69
|
+
<div
|
|
70
|
+
class="fixed top-1/2 left-1/2 z-50 w-72 -translate-x-1/2 -translate-y-1/2 rounded-lg border border-[var(--color-surface-border)] bg-[var(--color-surface-elevated)] px-3 py-2 shadow-lg backdrop-blur"
|
|
71
|
+
data-testid="chord-palette"
|
|
72
|
+
data-chord-elapsed={elapsed}
|
|
73
|
+
aria-live="polite"
|
|
74
|
+
aria-label={ariaLabel}
|
|
75
|
+
>
|
|
76
|
+
<div class="mb-1 text-[10px] font-semibold tracking-wide text-[var(--color-text-secondary)] uppercase">
|
|
77
|
+
{title}
|
|
78
|
+
</div>
|
|
79
|
+
<ul class="m-0 list-none space-y-0.5 p-0">
|
|
80
|
+
{#each rows as row (row.key)}
|
|
81
|
+
<li class="flex items-center gap-2 text-xs" data-testid="chord-palette-row" data-chord-key={row.key}>
|
|
82
|
+
<kbd class="min-w-5 rounded border border-[var(--color-surface-border)] bg-[var(--color-surface-secondary)] px-1 text-center font-mono text-[11px]">
|
|
83
|
+
{row.key}
|
|
84
|
+
</kbd>
|
|
85
|
+
<span class="text-[var(--color-text-primary)]">{row.label}</span>
|
|
86
|
+
</li>
|
|
87
|
+
{/each}
|
|
88
|
+
</ul>
|
|
89
|
+
<!-- The idle clock: in-action while the consumer's window is open, idle after -->
|
|
90
|
+
<div class="mt-2 h-1 w-full overflow-hidden rounded bg-[var(--color-surface-secondary)]" data-testid="chord-palette-decay">
|
|
91
|
+
<div
|
|
92
|
+
class="h-full {barColor} transition-[width] duration-100 ease-linear"
|
|
93
|
+
style="width: {pct}%"
|
|
94
|
+
data-testid="chord-palette-progress"
|
|
95
|
+
data-chord-phase={inActionWindow ? 'action' : 'idle'}
|
|
96
|
+
></div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|