diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0511f39 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,20 @@ +# Code of Conduct + +**What is a Code of Conduct?** +A code of conduct is a set of guidelines and expectations for behavior within a community. It helps create a positive and inclusive environment for all members of the community, and it sets clear standards for how members should interact with each other. A code of conduct can help prevent harassment, discrimination, and other forms of harmful behavior, and it can also provide a framework for addressing any issues that may arise. + +## Our Code of Conduct +We are committed to creating a welcoming and inclusive environment for all members of our community. We expect all members to adhere to the following code of conduct: + +**1. Be respectful:** Treat all members of the community with respect and kindness. Avoid using language or behavior that is offensive, discriminatory, or harassing. Some might be less experienced than others, so be patient and understanding. + +**2. Be inclusive:** We welcome and encourage participation from people of all backgrounds, identities, and experiences. Avoid making assumptions about others based on their race, gender, sexual orientation, religion, or any other characteristic. We also ask that you use inclusive language and avoid making jokes or comments that could be hurtful to others. + +**3. Be professional:** We expect all members of the community to conduct themselves in a professional manner. Avoid engaging in behavior that is disruptive, disrespectful, or unprofessional. This includes but is not limited to trolling, flaming, and spamming. + +**4. Be collaborative:** We encourage collaboration and teamwork within our community. Avoid engaging in behavior that is divisive or that undermines the efforts of others. This includes but is not limited to creating duplicate issues or pull requests, or engaging in arguments or conflicts with other members. + +**5. Report issues:** If you witness or experience any behavior that violates this code of conduct, please report it to the a maintainer or the authors of the library. We take all reports seriously and will take appropriate action to address any issues that arise. + +## Conclusion +We may update this code of conduct from time to time, and we encourage all members of our community to review it regularly. By adhering to this code of conduct, we can create a positive and inclusive environment for everyone in our community. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e69de29..8a056a5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,86 @@ +## Issue type + + +- [ ] Bug report +- [ ] New component request +- [ ] Feature request / improvement +- [ ] Other + +--- + + + +## Component affected + + + +## What happened? + +## What did you expect? + +## Steps to reproduce + +1. +2. +3. + +## Environment + +- **liquid-react version:** +- **React Native version:** +- **iOS version:** +- **Device / Simulator:** + +--- + + + +## UIKit class name + + + +## Suggested name in liquid-react + + + +## Why do you need it? + +## Props / API you'd expect + + + +## Minimum iOS version + +- [ ] This is part of Apple's **public** UIKit API (no private frameworks) + +--- + + + +## What do you want to improve? + +## What's the problem right now? + +## What would you like instead? + +- [ ] I believe this is doable with Apple's public APIs + +--- + +## Anything else? + + + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a5f494c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ +## What does this PR do? + + + +## Type of change + +- [ ] Bug fix +- [ ] New component +- [ ] Refactor / cleanup +- [ ] Docs update +- [ ] Other (describe above) + +## Related issue + +Closes # + +## Notes for reviewers + + + +--- + +## Checklist + +- [ ] Follows Code of Conduct and contributing guidelines +- [ ] If code changes were made, they were tested and verified to work + - [ ] I've updated the documentation to reflect said changes (if applicable) +- [ ] This is *not* a code change (e.g. a documentation update) +- [ ] Code passes CI/CD checks (if PR is ready for review, otherwise mark as draft) diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.github/workflows/lint-and-build.yml @@ -0,0 +1 @@ + diff --git a/.github/workflows/native-build.yml b/.github/workflows/native-build.yml new file mode 100644 index 0000000..14f068b --- /dev/null +++ b/.github/workflows/native-build.yml @@ -0,0 +1,55 @@ +name: Full Build Check + +on: + workflow_dispatch: + +jobs: + build: + name: TypeScript + Native Compilation + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install JS dependencies + run: npm ci --legacy-peer-deps + + - name: ESLint + run: npm run lint + + - name: Prettier check + run: npm run format:check + + - name: TypeScript type-check + run: npm run typecheck + + - name: TypeScript build + run: npm run build + + - name: Install SwiftLint + run: brew install swiftlint + + - name: SwiftLint + run: swiftlint lint --config .swiftlint.yml + + - name: Install CocoaPods + run: | + cd ios + pod install --repo-update || echo "No Podfile found, skipping pod install" + + - name: Build with xcodebuild + run: | + xcodebuild \ + -workspace ios/*.xcworkspace \ + -scheme LiquidReact \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + build \ + CODE_SIGNING_ALLOWED=NO | xcpretty || true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..be7a1ff --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,42 @@ +name: Publish to npm + +on: + push: + tags: + - 'v*.*.*' + +jobs: + publish: + name: Publish Package + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Lint + run: npm run lint + + - name: Type-check + run: npm run typecheck + + - name: Build + run: npm run build + + - name: Publish + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/swift-lint.yml b/.github/workflows/swift-lint.yml new file mode 100644 index 0000000..e1afdac --- /dev/null +++ b/.github/workflows/swift-lint.yml @@ -0,0 +1,24 @@ +name: SwiftLint + +on: + push: + branches: [main, staging, forProduction] + paths: + - 'ios/**' + pull_request: + paths: + - 'ios/**' + +jobs: + swiftlint: + name: Swift Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + with: + args: --config .swiftlint.yml diff --git a/.gitignore b/.gitignore index d8b6b62..c1bfcc7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ logs/ # Ignore IDE specific files .idea/ .vscode/ +.venv +package-lock.json diff --git a/index.js b/index.js index f9a99d0..740c492 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,2 @@ -// Export everything from src/index.ts -export * from './src/index'; +// Re-export compiled library output +module.exports = require('./lib/index'); diff --git a/ios/LRNativeMaterialViewManager.swift b/ios/LRNativeMaterialViewManager.swift index 83ca573..b736d7c 100644 --- a/ios/LRNativeMaterialViewManager.swift +++ b/ios/LRNativeMaterialViewManager.swift @@ -25,8 +25,6 @@ class NativeMaterialView: UIView { updateMaterial() } } - // ...existing code... - @objc var onPress: RCTBubblingEventBlock? override init(frame: CGRect) { diff --git a/ios/LRNativeToolbarButtonManager.swift b/ios/LRNativeToolbarButtonManager.swift index fcc1717..3facf60 100644 --- a/ios/LRNativeToolbarButtonManager.swift +++ b/ios/LRNativeToolbarButtonManager.swift @@ -95,6 +95,8 @@ class NativeToolbarButtonContainer: UIView { case "edit": type = .edit case "save": type = .save case "add": type = .add + case "flexibleSpace": type = .flexibleSpace + case "fixedSpace": type = .fixedSpace case "compose": type = .compose case "reply": type = .reply case "action": type = .action diff --git a/ios/LRNativeToolbarMenuManager.swift b/ios/LRNativeToolbarMenuManager.swift index 98010cd..4a141f6 100644 --- a/ios/LRNativeToolbarMenuManager.swift +++ b/ios/LRNativeToolbarMenuManager.swift @@ -48,8 +48,6 @@ class NativeToolbarMenuContainer: UIView { override init(frame: CGRect) { super.init(frame: frame) setupButton() - // Keep the view in the hierarchy (not hidden) so RCTDirectEventBlock - // can dispatch events to JS. Make it invisible instead. self.frame = CGRect(x: 0, y: 0, width: 1, height: 1) self.clipsToBounds = true self.alpha = 0 @@ -78,7 +76,8 @@ class NativeToolbarMenuContainer: UIView { } private func updateMenu() { - let menuElements = buildMenuElements(from: menuItems as! [[String: Any]]) + guard let items = menuItems as? [[String: Any]] else { return } + let menuElements = buildMenuElements(from: items) let menu = UIMenu(children: menuElements) barButtonItem.menu = menu } diff --git a/liquid-react.podspec b/liquid-react.podspec index 91ef0bc..1bada24 100644 --- a/liquid-react.podspec +++ b/liquid-react.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.homepage = package['repository']['url'] s.license = package['license'] s.author = package['author'] - s.platform = :ios, "13.0" + s.platform = :ios, "15.0" s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" } s.source_files = "ios/**/*.{h,m,swift}" s.requires_arc = true diff --git a/package.json b/package.json index 81e8866..f74b684 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,11 @@ "name": "liquid-react", "version": "1.0.0", "description": "Production-ready React Native iOS module exposing Apple's native UIKit Liquid Glass components", - "main": "index.js", - "types": "dist/index.d.ts", + "main": "lib/index.js", + "types": "lib/index.d.ts", "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build", "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint . --ext .js,.jsx,.ts,.tsx" }, @@ -28,11 +30,11 @@ "react-native": ">=0.65.0" }, "devDependencies": { - "@types/react": "^18.0.0", - "@types/react-native": "^0.73.0", + "@types/react": "^19.0.0", "typescript": "^5.0.0" }, "files": [ + "lib/", "src/", "ios/", "liquid-react.podspec", diff --git a/src/index.ts b/src/index.ts index 21e7f98..71dfa15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ export type { SearchBarChangeEvent, TabBarPressEvent, ToolbarMenuActionEvent, + ToolbarButtonPressEvent, NativeMaterialViewProps, NativeButtonProps, NativeSwitchProps, diff --git a/src/nativeComponents.ts b/src/nativeComponents.ts index 1b5b44e..9c28256 100644 --- a/src/nativeComponents.ts +++ b/src/nativeComponents.ts @@ -16,23 +16,35 @@ import type { NativeMenuButtonProps, } from './types'; -// On web, requireNativeComponent doesn't exist — fall back to View. -const req = Platform.OS === 'web' - ? () => View as any - : requireNativeComponent; +/** + * Resolves a native component by name. + * On web (Expo Web / react-native-web), falls back to a plain View + * to avoid a crash at module load time. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createNativeComponent(name: string): React.ComponentType { + if (Platform.OS === 'web') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return View as unknown as React.ComponentType; + } + // HostComponent is React.ComponentClass at runtime; the cast resolves + // dual-React-types version conflicts between the library and the host app. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return requireNativeComponent(name) as unknown as React.ComponentType; +} // Native component registration -export const RNNativeMaterialView = req('LRNativeMaterialView'); -export const RNNativeButton = req('LRNativeButton'); -export const RNNativeSwitch = req('LRNativeSwitch'); -export const RNNativeSegmentedControl = req('LRNativeSegmentedControl'); -export const RNNativeSearchBar = req('LRNativeSearchBar'); -export const RNNativeNavigationBar = req('LRNativeNavigationBar'); -export const RNNativeToolbar = req('LRNativeToolbar'); -export const RNNativeToolbarButton = req('LRNativeToolbarButton'); -export const RNNativeToolbarMenu = req('LRNativeToolbarMenu'); -export const RNNativeTabBar = req('LRNativeTabBar'); -export const RNNativeGroupedContainer = req('LRNativeGroupedContainer'); -export const RNNativeCardContainer = req('LRNativeCardContainer'); -export const RNNativeStackView = req('LRNativeStackView'); -export const RNNativeMenuButton = req('LRNativeMenuButton'); +export const RNNativeMaterialView = createNativeComponent('LRNativeMaterialView'); +export const RNNativeButton = createNativeComponent('LRNativeButton'); +export const RNNativeSwitch = createNativeComponent('LRNativeSwitch'); +export const RNNativeSegmentedControl = createNativeComponent('LRNativeSegmentedControl'); +export const RNNativeSearchBar = createNativeComponent('LRNativeSearchBar'); +export const RNNativeNavigationBar = createNativeComponent('LRNativeNavigationBar'); +export const RNNativeToolbar = createNativeComponent('LRNativeToolbar'); +export const RNNativeToolbarButton = createNativeComponent('LRNativeToolbarButton'); +export const RNNativeToolbarMenu = createNativeComponent('LRNativeToolbarMenu'); +export const RNNativeTabBar = createNativeComponent('LRNativeTabBar'); +export const RNNativeGroupedContainer = createNativeComponent('LRNativeGroupedContainer'); +export const RNNativeCardContainer = createNativeComponent('LRNativeCardContainer'); +export const RNNativeStackView = createNativeComponent('LRNativeStackView'); +export const RNNativeMenuButton = createNativeComponent('LRNativeMenuButton'); diff --git a/src/types.ts b/src/types.ts index 808f6a6..27d5cff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,8 @@ -import { requireNativeComponent, ViewProps } from 'react-native'; -import type { BubblingEventHandler, DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes'; +import type { ViewProps } from 'react-native'; +import type { ReactNode } from 'react'; + +type BubblingEventHandler = (event: T) => void; +type DirectEventHandler = (event: T) => void; // Material types export type MaterialType = @@ -65,6 +68,13 @@ disabled?: boolean; children?: ToolbarMenuItem[]; } +export interface ToolbarItem { + icon?: string; + title?: string; + systemItem?: ToolbarSystemItem; + disabled?: boolean; +} + export interface ToolbarMenuActionEvent { id: string; title: string; @@ -147,9 +157,19 @@ title?: string; translucent?: boolean; } +// Event carrying icon+title of the bar button that was tapped. +export interface ToolbarButtonPressEvent { + icon: string; + title: string; +} + export interface NativeToolbarProps extends ViewProps { -translucent?: boolean; -children?: React.ReactNode; + translucent?: boolean; + children?: ReactNode; + /** Fires when a NativeToolbarButton child is pressed. */ + onToolbarButtonPress?: DirectEventHandler; + /** Fires when a NativeToolbarMenu child action is triggered. */ + onToolbarMenuAction?: DirectEventHandler; } export interface NativeToolbarButtonProps extends ViewProps { diff --git a/tsconfig.json b/tsconfig.json index 0e1d6b6..bdd4c78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "ios"] + "exclude": ["node_modules", "lib", "dist", "ios"] }