@catcatcatstudio/cat-1a 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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * ModeBar — cat-1a theme switcher
3
+ *
4
+ * Manages colorway cycling (greige/mint) and brightness modes (light/dark).
5
+ * Works with any .mode-bar element. Persists state to localStorage.
6
+ * Sets data-theme on <html> for full-page theme switching.
7
+ *
8
+ * Usage:
9
+ * import { ModeBar } from '@catcatcat/cat-1a/mode-bar'
10
+ * const bar = ModeBar.init(document.querySelector('.mode-bar'))
11
+ *
12
+ * Or auto-init all mode bars on the page:
13
+ * ModeBar.initAll()
14
+ *
15
+ * Or via script tag:
16
+ * <script type="module" src="mode-bar.js"></script>
17
+ * // Auto-initializes all .mode-bar elements on DOMContentLoaded
18
+ */
19
+
20
+ const STORAGE_KEY = 'catalog-mode'
21
+
22
+ const DEFAULT_COLORWAYS = [
23
+ { name: 'greige', color: '#e2e1dc' },
24
+ { name: 'mint', color: '#e2ede5' },
25
+ ]
26
+
27
+ export class ModeBar {
28
+ constructor(el, options = {}) {
29
+ this.el = el
30
+ this.colorways = options.colorways || DEFAULT_COLORWAYS
31
+ this.storageKey = options.storageKey || STORAGE_KEY
32
+ this.onchange = options.onchange || null
33
+
34
+ // State
35
+ this.type = 'colorway' // 'colorway' | 'brightness'
36
+ this.cwIndex = 0
37
+ this.brightness = 'light'
38
+
39
+ this._restore()
40
+ this._bind()
41
+ this.sync()
42
+ }
43
+
44
+ /** Get the current active mode name */
45
+ get mode() {
46
+ return this.type === 'colorway'
47
+ ? this.colorways[this.cwIndex].name
48
+ : this.brightness
49
+ }
50
+
51
+ /** Sync DOM to current state */
52
+ sync() {
53
+ const mode = this.mode
54
+ const cw = this.colorways[this.cwIndex]
55
+
56
+ // Update bar
57
+ this.el.setAttribute('data-mode', mode)
58
+ const dot = this.el.querySelector('.mode-bar-dot')
59
+ if (dot) dot.style.background = cw.color
60
+
61
+ // Update document theme
62
+ if (mode === 'greige') {
63
+ document.documentElement.removeAttribute('data-theme')
64
+ } else {
65
+ document.documentElement.setAttribute('data-theme', mode)
66
+ }
67
+
68
+ // Persist
69
+ this._save()
70
+
71
+ // Callback
72
+ if (this.onchange) this.onchange(mode, this)
73
+ }
74
+
75
+ /** Trigger cycle pulse animation on the knob */
76
+ pulse() {
77
+ const knob = this.el.querySelector('.mode-bar-knob')
78
+ if (!knob) return
79
+ knob.classList.remove('cycling')
80
+ void knob.offsetWidth // force reflow
81
+ knob.classList.add('cycling')
82
+ knob.addEventListener('animationend', () => {
83
+ knob.classList.remove('cycling')
84
+ }, { once: true })
85
+ }
86
+
87
+ /** Destroy event listeners */
88
+ destroy() {
89
+ this.el.removeEventListener('click', this._handleClick)
90
+ }
91
+
92
+ // --- Private ---
93
+
94
+ _bind() {
95
+ this._handleClick = (e) => {
96
+ const stop = e.target.closest('[data-stop]')
97
+ if (!stop) return
98
+ const action = stop.dataset.stop
99
+
100
+ if (action === 'color') {
101
+ if (this.type === 'colorway') {
102
+ this.cwIndex = (this.cwIndex + 1) % this.colorways.length
103
+ this.sync()
104
+ this.pulse()
105
+ } else {
106
+ this.type = 'colorway'
107
+ this.sync()
108
+ }
109
+ return
110
+ }
111
+
112
+ // light or dark
113
+ this.type = 'brightness'
114
+ this.brightness = action
115
+ this.sync()
116
+ }
117
+
118
+ this.el.addEventListener('click', this._handleClick)
119
+ }
120
+
121
+ _save() {
122
+ try {
123
+ localStorage.setItem(this.storageKey, JSON.stringify({
124
+ ci: this.cwIndex,
125
+ tp: this.type,
126
+ br: this.brightness,
127
+ }))
128
+ } catch (e) { /* storage unavailable */ }
129
+ }
130
+
131
+ _restore() {
132
+ try {
133
+ const saved = JSON.parse(localStorage.getItem(this.storageKey))
134
+ if (saved) {
135
+ this.cwIndex = saved.ci || 0
136
+ this.type = saved.tp || 'colorway'
137
+ this.brightness = saved.br || 'light'
138
+ }
139
+ } catch (e) { /* storage unavailable */ }
140
+ }
141
+
142
+ // --- Static ---
143
+
144
+ /** Initialize a single mode bar element */
145
+ static init(el, options) {
146
+ if (!el) return null
147
+ return new ModeBar(el, options)
148
+ }
149
+
150
+ /** Initialize all .mode-bar elements on the page */
151
+ static initAll(options) {
152
+ return Array.from(document.querySelectorAll('.mode-bar'))
153
+ .map(el => new ModeBar(el, options))
154
+ }
155
+
156
+ /**
157
+ * Inline script for <head> — prevents flash of default theme on page load.
158
+ * Call ModeBar.earlyRestore() or paste the returned string into a <script> tag.
159
+ */
160
+ static get earlyRestoreScript() {
161
+ return `try{var s=JSON.parse(localStorage.getItem('${STORAGE_KEY}'));if(s){var m=s.tp==='colorway'?${JSON.stringify(DEFAULT_COLORWAYS)}[s.ci||0].name:s.br;if(m&&m!=='greige')document.documentElement.setAttribute('data-theme',m);}}catch(e){}`
162
+ }
163
+ }
164
+
165
+ // No auto-init. Consumers call ModeBar.init() or ModeBar.initAll() explicitly.
166
+ // Auto-init causes double-bind when pages also have inline scripts.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@catcatcatstudio/cat-1a",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.1.0",
7
+ "description": "cat-1a design system — tokens, components, and motion",
8
+ "license": "MIT",
9
+ "readme": "PKG-README.md",
10
+ "type": "module",
11
+ "exports": {
12
+ "./tokens.css": "./tokens/tokens.css",
13
+ "./components.css": "./components/components.css",
14
+ "./mode-bar": "./components/mode-bar.js"
15
+ },
16
+ "files": [
17
+ "tokens/tokens.css",
18
+ "components/components.css",
19
+ "components/mode-bar.js",
20
+ "PKG-README.md"
21
+ ],
22
+ "keywords": [
23
+ "design-system",
24
+ "catcatcat",
25
+ "cat-1a",
26
+ "css",
27
+ "tokens"
28
+ ]
29
+ }
@@ -0,0 +1,133 @@
1
+ /* cat-1a — catcatcat Design Tokens */
2
+ /* Import this file into any catcatcat project */
3
+
4
+ :root {
5
+ /* Color */
6
+ --bg: #e2e1dc;
7
+ --surface: #d9d8d3;
8
+ --surface-2: #d0cfca;
9
+ --border: #b8b7b2;
10
+ --text: #111;
11
+ --text-dim: #555;
12
+ --accent: #000;
13
+ --primary: var(--accent);
14
+
15
+ /* Feedback */
16
+ --feedback-info: #4285bb;
17
+ --feedback-success: #43a047;
18
+ --feedback-error: #dc3232;
19
+ --feedback-warn: #b48214;
20
+ --feedback-error-bg: rgba(220, 50, 50, 0.08);
21
+ --feedback-error-border: rgba(220, 50, 50, 0.45);
22
+ --feedback-success-bg: rgba(67, 160, 71, 0.1);
23
+ --feedback-success-border: rgba(67, 160, 71, 0.3);
24
+ --feedback-warn-bg: rgba(230, 160, 0, 0.1);
25
+ --feedback-warn-border: rgba(230, 160, 0, 0.2);
26
+
27
+ /* Spacing */
28
+ --space-1: 4px; /* micro: icon-text, tight pairs */
29
+ --space-2: 6px; /* compact: nav items, small gaps */
30
+ --space-3: 8px; /* base: button padding, inline gaps */
31
+ --space-4: 12px; /* comfortable: form fields, card internals */
32
+ --space-5: 16px; /* spacious: container padding, section gaps */
33
+ --space-6: 20px; /* regional: panel padding, major gaps */
34
+ --space-7: 24px; /* sectional: between content groups */
35
+ --space-8: 32px; /* page: showcase areas, hero padding */
36
+ --space-9: 48px; /* landmark: section dividers, page margins */
37
+
38
+ /* Radius */
39
+ --radius: 8px;
40
+ --radius-lg: 14px;
41
+
42
+ /* Typography */
43
+ --font: "IBM Plex Mono", monospace;
44
+ --font-size-xs: 10px;
45
+ --font-size-sm: 11px;
46
+ --font-size-base: 13px;
47
+ --font-size-md: 14px;
48
+ --font-size-lg: 15px;
49
+ --text-transform: uppercase;
50
+ --letter-spacing: 0.02em;
51
+ --letter-spacing-wide: 0.08em;
52
+
53
+ /* Motion */
54
+ --ease: cubic-bezier(0.4, 0, 0.2, 1);
55
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
56
+ --duration-fast: 200ms;
57
+ --duration-base: 250ms;
58
+ --duration-slow: 300ms;
59
+
60
+ /* Elevation */
61
+ --shadow-floating: 0 2px 12px rgba(0, 0, 0, 0.06);
62
+ --shadow-overlay: 0 8px 32px rgba(0, 0, 0, 0.12);
63
+ --shadow-knob: 0 1px 3px rgba(0, 0, 0, 0.08);
64
+
65
+ /* Surface Material */
66
+ --surface-alpha: 1;
67
+ --surface-blur: 0px;
68
+ --surface-2-alpha: 1;
69
+ --surface-2-blur: 0px;
70
+
71
+ /* Interaction */
72
+ --tint: rgba(0, 0, 0, 0.06);
73
+ --tint-subtle: rgba(0, 0, 0, 0.03);
74
+
75
+ /* Overlay */
76
+ --overlay-bg: rgba(0, 0, 0, 0.3);
77
+ }
78
+
79
+
80
+ /* ---- Mint Colorway ---- */
81
+ /* Soft green identity tint. Same structure as default greige. */
82
+
83
+ [data-theme="mint"] {
84
+ --bg: #e2ede5;
85
+ --surface: #d7e3da;
86
+ --surface-2: #ccd9cf;
87
+ --border: #b8c9bb;
88
+ }
89
+
90
+
91
+ /* ---- Light Mode ---- */
92
+ /* Black on white. Text and accent stay the same (both dark-on-light). */
93
+
94
+ [data-theme="light"] {
95
+ --bg: #fff;
96
+ --surface: #f5f5f4;
97
+ --surface-2: #edeceb;
98
+ --border: #d6d5d3;
99
+ }
100
+
101
+
102
+ /* ---- Dark Mode ---- */
103
+ /* White on near-black. Accent flips to white. Shadows deepen for visibility. */
104
+
105
+ [data-theme="dark"] {
106
+ --bg: #111;
107
+ --surface: #1a1a1a;
108
+ --surface-2: #242424;
109
+ --border: #333;
110
+ --text: #e5e5e5;
111
+ --text-dim: #888;
112
+ --accent: #fff;
113
+
114
+ --feedback-info: #64b5f6;
115
+ --feedback-success: #66bb6a;
116
+ --feedback-error: #ef5350;
117
+ --feedback-warn: #e6a000;
118
+
119
+ --shadow-floating: 0 2px 12px rgba(0, 0, 0, 0.3);
120
+ --shadow-overlay: 0 8px 32px rgba(0, 0, 0, 0.5);
121
+ --shadow-knob: 0 1px 3px rgba(0, 0, 0, 0.4);
122
+
123
+ --tint: rgba(255, 255, 255, 0.06);
124
+ --tint-subtle: rgba(255, 255, 255, 0.03);
125
+
126
+ --feedback-error-bg: rgba(239, 83, 80, 0.12);
127
+ --feedback-error-border: rgba(239, 83, 80, 0.45);
128
+ --feedback-success-bg: rgba(102, 187, 106, 0.12);
129
+ --feedback-success-border: rgba(102, 187, 106, 0.3);
130
+ --feedback-warn-bg: rgba(230, 160, 0, 0.12);
131
+ --feedback-warn-border: rgba(230, 160, 0, 0.25);
132
+ --overlay-bg: rgba(0, 0, 0, 0.55);
133
+ }