react-native-control-center 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/LICENSE +21 -0
- package/README.md +415 -0
- package/app.plugin.js +14 -0
- package/cli/bin/rn-control-center.js +52 -0
- package/ios/ControlStoreRuntime.swift +81 -0
- package/ios/RNControlCenter.mm +28 -0
- package/ios/RNControlCenter.swift +195 -0
- package/lib/commonjs/cli/runGenerate.d.ts +22 -0
- package/lib/commonjs/cli/runGenerate.js +173 -0
- package/lib/commonjs/core/generate/entitlements.d.ts +17 -0
- package/lib/commonjs/core/generate/entitlements.js +31 -0
- package/lib/commonjs/core/generate/index.d.ts +30 -0
- package/lib/commonjs/core/generate/index.js +58 -0
- package/lib/commonjs/core/generate/plist.d.ts +13 -0
- package/lib/commonjs/core/generate/plist.js +37 -0
- package/lib/commonjs/core/generate/swift.d.ts +22 -0
- package/lib/commonjs/core/generate/swift.js +140 -0
- package/lib/commonjs/core/parseControls.d.ts +9 -0
- package/lib/commonjs/core/parseControls.js +206 -0
- package/lib/commonjs/core/sf-symbols-data.d.ts +3 -0
- package/lib/commonjs/core/sf-symbols-data.js +5373 -0
- package/lib/commonjs/core/templates/ButtonControl.swift.hbs +28 -0
- package/lib/commonjs/core/templates/ButtonIntent.swift.hbs +39 -0
- package/lib/commonjs/core/templates/ControlBundle.swift.hbs +17 -0
- package/lib/commonjs/core/templates/ControlStore.swift.hbs +149 -0
- package/lib/commonjs/core/templates/ToggleControl.swift.hbs +60 -0
- package/lib/commonjs/core/templates/ToggleIntent.swift.hbs +49 -0
- package/lib/commonjs/core/types.d.ts +14 -0
- package/lib/commonjs/core/types.js +17 -0
- package/lib/commonjs/core/validateSymbols.d.ts +15 -0
- package/lib/commonjs/core/validateSymbols.js +43 -0
- package/lib/commonjs/core/xcode/addSyncedFolder.d.ts +28 -0
- package/lib/commonjs/core/xcode/addSyncedFolder.js +71 -0
- package/lib/commonjs/core/xcode/addTarget.d.ts +25 -0
- package/lib/commonjs/core/xcode/addTarget.js +34 -0
- package/lib/commonjs/core/xcode/buildSettings.d.ts +14 -0
- package/lib/commonjs/core/xcode/buildSettings.js +57 -0
- package/lib/commonjs/core/xcode/embed.d.ts +16 -0
- package/lib/commonjs/core/xcode/embed.js +74 -0
- package/lib/commonjs/core/xcode/inspect.d.ts +29 -0
- package/lib/commonjs/core/xcode/inspect.js +87 -0
- package/lib/commonjs/core/xcode/linkFrameworks.d.ts +18 -0
- package/lib/commonjs/core/xcode/linkFrameworks.js +80 -0
- package/lib/commonjs/core/xcode/types.d.ts +121 -0
- package/lib/commonjs/core/xcode/types.js +7 -0
- package/lib/commonjs/core/xcode/wire.d.ts +27 -0
- package/lib/commonjs/core/xcode/wire.js +142 -0
- package/lib/commonjs/plugin/index.d.ts +43 -0
- package/lib/commonjs/plugin/index.js +177 -0
- package/lib/commonjs/src/ControlCenter.d.ts +34 -0
- package/lib/commonjs/src/ControlCenter.js +91 -0
- package/lib/commonjs/src/defineControls.d.ts +6 -0
- package/lib/commonjs/src/defineControls.js +10 -0
- package/lib/commonjs/src/hooks.d.ts +8 -0
- package/lib/commonjs/src/hooks.js +38 -0
- package/lib/commonjs/src/index.d.ts +5 -0
- package/lib/commonjs/src/index.js +9 -0
- package/lib/commonjs/src/sf-symbols.d.ts +8 -0
- package/lib/commonjs/src/sf-symbols.js +2 -0
- package/lib/commonjs/src/stateCache.d.ts +8 -0
- package/lib/commonjs/src/stateCache.js +36 -0
- package/lib/commonjs/src/types.d.ts +36 -0
- package/lib/commonjs/src/types.js +2 -0
- package/package.json +75 -0
- package/src/ControlCenter.ts +122 -0
- package/src/defineControls.ts +9 -0
- package/src/hooks.ts +42 -0
- package/src/index.ts +12 -0
- package/src/sf-symbols.ts +251 -0
- package/src/stateCache.ts +34 -0
- package/src/types.ts +36 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
|
|
2
|
+
import { getCachedState, setCachedState, seedCache } from './stateCache';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// ControlCenter — Native Module JS wrapper
|
|
6
|
+
//
|
|
7
|
+
// Swift의 RNControlCenter가 발사하는 이벤트를 받고,
|
|
8
|
+
// 메서드 호출(getState/setState)을 Promise로 노출한다.
|
|
9
|
+
//
|
|
10
|
+
// iOS 외 플랫폼 또는 Native Module 미설치 시 모든 메서드는 no-op.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const RNControlCenter = NativeModules.RNControlCenter as
|
|
14
|
+
| {
|
|
15
|
+
getState(key: string): Promise<unknown>;
|
|
16
|
+
setState(key: string, value: unknown): Promise<void>;
|
|
17
|
+
// 네이티브가 모듈 등록 시점에 동기로 넘겨주는 상수.
|
|
18
|
+
// 콜드 스타트 직후에도 첫 렌더에서 바로 쓸 수 있는 초기 상태 스냅샷.
|
|
19
|
+
initialState?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
| undefined;
|
|
22
|
+
|
|
23
|
+
interface ControlActionEvent {
|
|
24
|
+
id: string;
|
|
25
|
+
deepLink?: string;
|
|
26
|
+
t: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ControlStateChangeEvent {
|
|
30
|
+
key: string;
|
|
31
|
+
value: unknown;
|
|
32
|
+
t: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Unsubscribe = () => void;
|
|
36
|
+
|
|
37
|
+
class ControlCenterAPI {
|
|
38
|
+
private emitter: NativeEventEmitter | null;
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
if (Platform.OS === 'ios' && RNControlCenter) {
|
|
42
|
+
// NativeEventEmitter는 NativeModule을 받아 startObserving/stopObserving을
|
|
43
|
+
// 자동으로 호출해 준다. addListener가 첫 등록되는 순간 Swift의
|
|
44
|
+
// startObserving이 발사되고, 마지막 listener가 제거되면 stopObserving이 발사된다.
|
|
45
|
+
this.emitter = new NativeEventEmitter(NativeModules.RNControlCenter);
|
|
46
|
+
|
|
47
|
+
// 콜드 스타트 시드 — 네이티브가 넘겨준 초기 스냅샷으로 캐시를 미리 채운다.
|
|
48
|
+
seedCache(RNControlCenter.initialState);
|
|
49
|
+
|
|
50
|
+
// 모든 ControlStateChange 이벤트를 캐시에 반영하는 내부 리스너.
|
|
51
|
+
// 키별 구독(onStateChange)과 별개로, 어떤 키가 바뀌든 캐시는 항상 최신.
|
|
52
|
+
this.emitter.addListener(
|
|
53
|
+
'ControlStateChange',
|
|
54
|
+
(event: ControlStateChangeEvent) => {
|
|
55
|
+
setCachedState(event.key, event.value);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
this.emitter = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 캐시된 마지막 값을 동기로 반환. 모르면 undefined.
|
|
65
|
+
* useControlState 훅이 첫 렌더 초기값으로 사용 (깜빡임 방지).
|
|
66
|
+
*/
|
|
67
|
+
getCachedState<T>(key: string): T | undefined {
|
|
68
|
+
return getCachedState<T>(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 라이브러리가 현재 환경에서 실제로 동작 가능한지 (iOS + Native Module 로드됨). */
|
|
72
|
+
isAvailable(): boolean {
|
|
73
|
+
return this.emitter !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 사용자가 제어센터의 Button을 탭했을 때 발사되는 이벤트 구독.
|
|
78
|
+
* @returns unsubscribe 함수
|
|
79
|
+
*/
|
|
80
|
+
onAction(cb: (event: ControlActionEvent) => void): Unsubscribe {
|
|
81
|
+
if (!this.emitter) return () => {};
|
|
82
|
+
const sub = this.emitter.addListener('ControlAction', cb);
|
|
83
|
+
return () => sub.remove();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 특정 stateKey의 값이 바뀌었을 때 발사되는 이벤트 구독.
|
|
88
|
+
* Swift는 모든 키를 하나의 이벤트로 발사하므로 여기서 키 필터링.
|
|
89
|
+
* @returns unsubscribe 함수
|
|
90
|
+
*/
|
|
91
|
+
onStateChange<T>(key: string, cb: (value: T) => void): Unsubscribe {
|
|
92
|
+
if (!this.emitter) return () => {};
|
|
93
|
+
const sub = this.emitter.addListener(
|
|
94
|
+
'ControlStateChange',
|
|
95
|
+
(event: ControlStateChangeEvent) => {
|
|
96
|
+
if (event.key === key) cb(event.value as T);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
return () => sub.remove();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** App Group UserDefaults에서 값 읽기. iOS 외에선 null. */
|
|
103
|
+
async getState<T>(key: string): Promise<T | null> {
|
|
104
|
+
if (!RNControlCenter) return null;
|
|
105
|
+
try {
|
|
106
|
+
const value = await RNControlCenter.getState(key);
|
|
107
|
+
setCachedState(key, value); // 응답을 캐시에 반영
|
|
108
|
+
return value as T;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** App Group UserDefaults에 값 쓰기. iOS 외에선 no-op. */
|
|
115
|
+
async setState<T>(key: string, value: T): Promise<void> {
|
|
116
|
+
setCachedState(key, value); // optimistic — 네이티브 왕복 전에 캐시 먼저 갱신
|
|
117
|
+
if (!RNControlCenter) return;
|
|
118
|
+
await RNControlCenter.setState(key, value as unknown);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const ControlCenter = new ControlCenterAPI();
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { ControlCenter } from './ControlCenter';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* App Group에 저장된 control 상태에 React 친화적 접근을 제공.
|
|
6
|
+
*
|
|
7
|
+
* Week 6: 캐시 레이어로 sync 초기값 보장.
|
|
8
|
+
* 첫 렌더에서 ControlCenter의 캐시(네이티브 initialState 시드 + 직전 값)를
|
|
9
|
+
* 동기로 읽어 깜빡임을 없앤다. 캐시에 없으면 null로 시작해 getState 응답을 기다림.
|
|
10
|
+
*/
|
|
11
|
+
export function useControlState<T>(key: string): [T | null, (value: T) => void] {
|
|
12
|
+
const [value, setValue] = useState<T | null>(() => {
|
|
13
|
+
const cached = ControlCenter.getCachedState<T>(key);
|
|
14
|
+
return cached !== undefined ? cached : null;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// 초기값 — native에 비동기로 물어봄 (캐시가 오래됐을 수 있으니 최신값으로 보정)
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
ControlCenter.getState<T>(key).then((v) => {
|
|
21
|
+
if (!cancelled) setValue(v);
|
|
22
|
+
});
|
|
23
|
+
return () => {
|
|
24
|
+
cancelled = true;
|
|
25
|
+
};
|
|
26
|
+
}, [key]);
|
|
27
|
+
|
|
28
|
+
// 변경 이벤트 구독
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
return ControlCenter.onStateChange<T>(key, (newVal) => setValue(newVal));
|
|
31
|
+
}, [key]);
|
|
32
|
+
|
|
33
|
+
const setter = useCallback(
|
|
34
|
+
(newVal: T) => {
|
|
35
|
+
ControlCenter.setState(key, newVal);
|
|
36
|
+
setValue(newVal); // optimistic update — 다음 이벤트가 같은 값을 다시 보내도 무해
|
|
37
|
+
},
|
|
38
|
+
[key]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return [value, setter];
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { defineControls } from './defineControls';
|
|
2
|
+
export { ControlCenter } from './ControlCenter';
|
|
3
|
+
export { useControlState } from './hooks';
|
|
4
|
+
export type {
|
|
5
|
+
ButtonControl,
|
|
6
|
+
ToggleControl,
|
|
7
|
+
Control,
|
|
8
|
+
SFSymbolName,
|
|
9
|
+
StrictSFSymbolName,
|
|
10
|
+
HexColor,
|
|
11
|
+
} from './types';
|
|
12
|
+
export type { KnownSFSymbol } from './sf-symbols';
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 자주 쓰이는 SF Symbols 큐레이션 리스트.
|
|
3
|
+
* 자동완성용 — 실제 타입은 string도 허용합니다.
|
|
4
|
+
*
|
|
5
|
+
* 전체 5000+ 심볼은 https://developer.apple.com/sf-symbols/ 참조.
|
|
6
|
+
* Xcode → SF Symbols.app 에서 원하는 심볼 이름 검색 가능.
|
|
7
|
+
*/
|
|
8
|
+
export type KnownSFSymbol =
|
|
9
|
+
// Actions
|
|
10
|
+
| 'square.and.pencil'
|
|
11
|
+
| 'pencil'
|
|
12
|
+
| 'pencil.circle'
|
|
13
|
+
| 'pencil.circle.fill'
|
|
14
|
+
| 'plus'
|
|
15
|
+
| 'plus.circle'
|
|
16
|
+
| 'plus.circle.fill'
|
|
17
|
+
| 'minus'
|
|
18
|
+
| 'minus.circle'
|
|
19
|
+
| 'minus.circle.fill'
|
|
20
|
+
| 'xmark'
|
|
21
|
+
| 'xmark.circle'
|
|
22
|
+
| 'xmark.circle.fill'
|
|
23
|
+
| 'checkmark'
|
|
24
|
+
| 'checkmark.circle'
|
|
25
|
+
| 'checkmark.circle.fill'
|
|
26
|
+
| 'trash'
|
|
27
|
+
| 'trash.fill'
|
|
28
|
+
// Navigation
|
|
29
|
+
| 'chevron.left'
|
|
30
|
+
| 'chevron.right'
|
|
31
|
+
| 'chevron.up'
|
|
32
|
+
| 'chevron.down'
|
|
33
|
+
| 'arrow.left'
|
|
34
|
+
| 'arrow.right'
|
|
35
|
+
| 'arrow.up'
|
|
36
|
+
| 'arrow.down'
|
|
37
|
+
| 'arrow.clockwise'
|
|
38
|
+
| 'arrow.counterclockwise'
|
|
39
|
+
// Sharing & Communication
|
|
40
|
+
| 'square.and.arrow.up'
|
|
41
|
+
| 'square.and.arrow.down'
|
|
42
|
+
| 'paperplane'
|
|
43
|
+
| 'paperplane.fill'
|
|
44
|
+
| 'envelope'
|
|
45
|
+
| 'envelope.fill'
|
|
46
|
+
| 'bubble.left'
|
|
47
|
+
| 'bubble.right'
|
|
48
|
+
| 'phone'
|
|
49
|
+
| 'phone.fill'
|
|
50
|
+
// Media
|
|
51
|
+
| 'play'
|
|
52
|
+
| 'play.fill'
|
|
53
|
+
| 'play.circle'
|
|
54
|
+
| 'play.circle.fill'
|
|
55
|
+
| 'pause'
|
|
56
|
+
| 'pause.fill'
|
|
57
|
+
| 'pause.circle'
|
|
58
|
+
| 'pause.circle.fill'
|
|
59
|
+
| 'stop'
|
|
60
|
+
| 'stop.fill'
|
|
61
|
+
| 'forward'
|
|
62
|
+
| 'forward.fill'
|
|
63
|
+
| 'backward'
|
|
64
|
+
| 'backward.fill'
|
|
65
|
+
| 'speaker'
|
|
66
|
+
| 'speaker.fill'
|
|
67
|
+
| 'speaker.slash'
|
|
68
|
+
| 'speaker.slash.fill'
|
|
69
|
+
| 'mic'
|
|
70
|
+
| 'mic.fill'
|
|
71
|
+
| 'mic.slash'
|
|
72
|
+
// System
|
|
73
|
+
| 'gear'
|
|
74
|
+
| 'gearshape'
|
|
75
|
+
| 'gearshape.fill'
|
|
76
|
+
| 'power'
|
|
77
|
+
| 'bolt'
|
|
78
|
+
| 'bolt.fill'
|
|
79
|
+
| 'bolt.slash'
|
|
80
|
+
| 'bolt.slash.fill'
|
|
81
|
+
| 'battery.100'
|
|
82
|
+
| 'battery.75'
|
|
83
|
+
| 'battery.50'
|
|
84
|
+
| 'battery.25'
|
|
85
|
+
| 'battery.0'
|
|
86
|
+
// Privacy & Security
|
|
87
|
+
| 'lock'
|
|
88
|
+
| 'lock.fill'
|
|
89
|
+
| 'lock.open'
|
|
90
|
+
| 'lock.open.fill'
|
|
91
|
+
| 'lock.shield'
|
|
92
|
+
| 'lock.shield.fill'
|
|
93
|
+
| 'key'
|
|
94
|
+
| 'key.fill'
|
|
95
|
+
| 'shield'
|
|
96
|
+
| 'shield.fill'
|
|
97
|
+
| 'shield.slash'
|
|
98
|
+
| 'eye'
|
|
99
|
+
| 'eye.fill'
|
|
100
|
+
| 'eye.slash'
|
|
101
|
+
| 'eye.slash.fill'
|
|
102
|
+
// User & Social
|
|
103
|
+
| 'person'
|
|
104
|
+
| 'person.fill'
|
|
105
|
+
| 'person.circle'
|
|
106
|
+
| 'person.circle.fill'
|
|
107
|
+
| 'person.2'
|
|
108
|
+
| 'person.2.fill'
|
|
109
|
+
| 'person.3'
|
|
110
|
+
| 'person.crop.circle'
|
|
111
|
+
| 'person.crop.circle.fill'
|
|
112
|
+
// Content
|
|
113
|
+
| 'doc'
|
|
114
|
+
| 'doc.fill'
|
|
115
|
+
| 'doc.text'
|
|
116
|
+
| 'doc.text.fill'
|
|
117
|
+
| 'note'
|
|
118
|
+
| 'note.text'
|
|
119
|
+
| 'folder'
|
|
120
|
+
| 'folder.fill'
|
|
121
|
+
| 'book'
|
|
122
|
+
| 'book.fill'
|
|
123
|
+
| 'bookmark'
|
|
124
|
+
| 'bookmark.fill'
|
|
125
|
+
| 'tag'
|
|
126
|
+
| 'tag.fill'
|
|
127
|
+
// Status
|
|
128
|
+
| 'heart'
|
|
129
|
+
| 'heart.fill'
|
|
130
|
+
| 'heart.slash'
|
|
131
|
+
| 'star'
|
|
132
|
+
| 'star.fill'
|
|
133
|
+
| 'star.slash'
|
|
134
|
+
| 'flag'
|
|
135
|
+
| 'flag.fill'
|
|
136
|
+
| 'flag.slash'
|
|
137
|
+
| 'bell'
|
|
138
|
+
| 'bell.fill'
|
|
139
|
+
| 'bell.slash'
|
|
140
|
+
| 'bell.slash.fill'
|
|
141
|
+
// Connectivity
|
|
142
|
+
| 'wifi'
|
|
143
|
+
| 'wifi.slash'
|
|
144
|
+
| 'antenna.radiowaves.left.and.right'
|
|
145
|
+
| 'airplane'
|
|
146
|
+
| 'airplayaudio'
|
|
147
|
+
| 'airplayvideo'
|
|
148
|
+
| 'network'
|
|
149
|
+
| 'globe'
|
|
150
|
+
// Location & Maps
|
|
151
|
+
| 'location'
|
|
152
|
+
| 'location.fill'
|
|
153
|
+
| 'location.slash'
|
|
154
|
+
| 'mappin'
|
|
155
|
+
| 'mappin.circle.fill'
|
|
156
|
+
| 'map'
|
|
157
|
+
| 'map.fill'
|
|
158
|
+
| 'house'
|
|
159
|
+
| 'house.fill'
|
|
160
|
+
| 'building'
|
|
161
|
+
| 'building.2'
|
|
162
|
+
// Time
|
|
163
|
+
| 'clock'
|
|
164
|
+
| 'clock.fill'
|
|
165
|
+
| 'alarm'
|
|
166
|
+
| 'alarm.fill'
|
|
167
|
+
| 'timer'
|
|
168
|
+
| 'stopwatch'
|
|
169
|
+
| 'stopwatch.fill'
|
|
170
|
+
| 'calendar'
|
|
171
|
+
| 'calendar.badge.plus'
|
|
172
|
+
| 'calendar.badge.clock'
|
|
173
|
+
// Camera & Photo
|
|
174
|
+
| 'camera'
|
|
175
|
+
| 'camera.fill'
|
|
176
|
+
| 'camera.circle'
|
|
177
|
+
| 'camera.circle.fill'
|
|
178
|
+
| 'photo'
|
|
179
|
+
| 'photo.fill'
|
|
180
|
+
| 'photo.on.rectangle'
|
|
181
|
+
| 'video'
|
|
182
|
+
| 'video.fill'
|
|
183
|
+
| 'video.slash'
|
|
184
|
+
// Payment & Shopping
|
|
185
|
+
| 'creditcard'
|
|
186
|
+
| 'creditcard.fill'
|
|
187
|
+
| 'cart'
|
|
188
|
+
| 'cart.fill'
|
|
189
|
+
| 'bag'
|
|
190
|
+
| 'bag.fill'
|
|
191
|
+
| 'dollarsign.circle'
|
|
192
|
+
| 'dollarsign.circle.fill'
|
|
193
|
+
// Search & Info
|
|
194
|
+
| 'magnifyingglass'
|
|
195
|
+
| 'magnifyingglass.circle'
|
|
196
|
+
| 'info'
|
|
197
|
+
| 'info.circle'
|
|
198
|
+
| 'info.circle.fill'
|
|
199
|
+
| 'questionmark'
|
|
200
|
+
| 'questionmark.circle'
|
|
201
|
+
| 'questionmark.circle.fill'
|
|
202
|
+
| 'exclamationmark'
|
|
203
|
+
| 'exclamationmark.circle'
|
|
204
|
+
| 'exclamationmark.triangle'
|
|
205
|
+
| 'exclamationmark.triangle.fill'
|
|
206
|
+
// Health & Fitness
|
|
207
|
+
| 'heart.text.square'
|
|
208
|
+
| 'figure.walk'
|
|
209
|
+
| 'figure.run'
|
|
210
|
+
| 'figure.stand'
|
|
211
|
+
| 'flame'
|
|
212
|
+
| 'flame.fill'
|
|
213
|
+
| 'drop'
|
|
214
|
+
| 'drop.fill'
|
|
215
|
+
| 'leaf'
|
|
216
|
+
| 'leaf.fill'
|
|
217
|
+
// Weather
|
|
218
|
+
| 'sun.max'
|
|
219
|
+
| 'sun.max.fill'
|
|
220
|
+
| 'moon'
|
|
221
|
+
| 'moon.fill'
|
|
222
|
+
| 'moon.stars'
|
|
223
|
+
| 'moon.stars.fill'
|
|
224
|
+
| 'cloud'
|
|
225
|
+
| 'cloud.fill'
|
|
226
|
+
| 'cloud.rain'
|
|
227
|
+
| 'cloud.rain.fill'
|
|
228
|
+
| 'snowflake'
|
|
229
|
+
// Home & Devices
|
|
230
|
+
| 'tv'
|
|
231
|
+
| 'tv.fill'
|
|
232
|
+
| 'lightbulb'
|
|
233
|
+
| 'lightbulb.fill'
|
|
234
|
+
| 'lightbulb.slash'
|
|
235
|
+
| 'thermometer'
|
|
236
|
+
| 'fan'
|
|
237
|
+
| 'air.conditioner.horizontal'
|
|
238
|
+
// Layouts
|
|
239
|
+
| 'rectangle.stack'
|
|
240
|
+
| 'square.stack'
|
|
241
|
+
| 'square.grid.2x2'
|
|
242
|
+
| 'square.grid.3x3'
|
|
243
|
+
| 'list.bullet'
|
|
244
|
+
| 'list.dash'
|
|
245
|
+
// Misc
|
|
246
|
+
| 'ellipsis'
|
|
247
|
+
| 'ellipsis.circle'
|
|
248
|
+
| 'line.3.horizontal'
|
|
249
|
+
| 'line.3.horizontal.decrease'
|
|
250
|
+
| 'slider.horizontal.3'
|
|
251
|
+
| 'arrow.up.arrow.down';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// stateCache — control 상태의 동기 보관함 (Week 6)
|
|
3
|
+
//
|
|
4
|
+
// getState는 Promise라 값이 "다음 tick"에 도착한다. 그 사이 UI가 null로
|
|
5
|
+
// 깜빡이는 걸 막기 위해, 마지막으로 알던 값을 모듈 레벨 Map에 둔다.
|
|
6
|
+
// useControlState 훅이 첫 렌더에서 이 캐시를 동기로 읽어 바로 표시한다.
|
|
7
|
+
//
|
|
8
|
+
// react-native에 의존하지 않는 순수 모듈 — 그래서 node 환경에서 단독 테스트 가능.
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const cache = new Map<string, unknown>();
|
|
12
|
+
|
|
13
|
+
/** 캐시된 마지막 값을 동기로 반환. 한 번도 본 적 없으면 undefined. */
|
|
14
|
+
export function getCachedState<T>(key: string): T | undefined {
|
|
15
|
+
return cache.has(key) ? (cache.get(key) as T) : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 새 값을 캐시에 기록 (getState 응답 / setState / onStateChange 이벤트에서 호출). */
|
|
19
|
+
export function setCachedState(key: string, value: unknown): void {
|
|
20
|
+
cache.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 네이티브 initialState 스냅샷으로 캐시를 한 번에 채움 (콜드 스타트 시드). */
|
|
24
|
+
export function seedCache(initial: Record<string, unknown> | undefined): void {
|
|
25
|
+
if (!initial) return;
|
|
26
|
+
for (const key of Object.keys(initial)) {
|
|
27
|
+
cache.set(key, initial[key]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 테스트 전용 — 캐시를 비운다. */
|
|
32
|
+
export function __resetCacheForTests(): void {
|
|
33
|
+
cache.clear();
|
|
34
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KnownSFSymbol } from './sf-symbols';
|
|
2
|
+
|
|
3
|
+
export type HexColor = `#${string}`;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 유연 모드: 큐레이션된 ~200개는 자동완성 + 그 외 문자열도 허용.
|
|
7
|
+
* TypeScript 트릭: `(string & {})` 는 자동완성을 비활성화하지 않으면서
|
|
8
|
+
* 임의 문자열을 받게 해줌.
|
|
9
|
+
*/
|
|
10
|
+
export type SFSymbolName = KnownSFSymbol | (string & {});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 엄격 모드: 큐레이션 리스트에 있는 심볼만 허용.
|
|
14
|
+
* 오타/잘못된 이름 방지가 중요할 때 사용.
|
|
15
|
+
*/
|
|
16
|
+
export type StrictSFSymbolName = KnownSFSymbol;
|
|
17
|
+
|
|
18
|
+
export interface ButtonControl {
|
|
19
|
+
type: 'button';
|
|
20
|
+
title: string;
|
|
21
|
+
icon: SFSymbolName;
|
|
22
|
+
tint?: HexColor;
|
|
23
|
+
description?: string;
|
|
24
|
+
deepLink?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToggleControl {
|
|
28
|
+
type: 'toggle';
|
|
29
|
+
title: string;
|
|
30
|
+
icons: { on: SFSymbolName; off: SFSymbolName };
|
|
31
|
+
tint?: { on: HexColor; off: HexColor };
|
|
32
|
+
stateKey: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Control = ButtonControl | ToggleControl;
|