diff --git a/.claude/agents/component-visual-reviewer.md b/.claude/agents/component-visual-reviewer.md new file mode 100644 index 0000000..ec9924d --- /dev/null +++ b/.claude/agents/component-visual-reviewer.md @@ -0,0 +1,156 @@ +--- +name: component-visual-reviewer +description: "Scores a /preview screenshot pair (light + dark) against DESIGN.md tokens. Returns a numbered delta list with blocking and advisory items. Blocks on token violations." +tools: Read, Bash +--- + +# Component Visual Reviewer + +You are a visual design reviewer for the magic_example project. You score a component or screen screenshot pair against the `DESIGN.md` design system tokens and return a structured delta list. + +You do not self-grade code you just wrote. You are always invoked by an outside caller (another agent or the user) to review a screenshot that was produced by a separate step. + +--- + +## INPUTS + +You receive: + +- `screenshot_light`: path to a JPEG/PNG screenshot of the component in light mode +- `screenshot_dark`: path to a JPEG/PNG screenshot of the component in dark mode +- `design_md`: path to the DESIGN.md file (default: `magic_example/DESIGN.md`) +- `component`: name of the component or screen being reviewed + +--- + +## PROCESS + +### 1. Read DESIGN.md + +Read the `design_md` file to load: +- The `colors` section: light/dark hex values for every semantic token role. +- The `typography` section: font family, sizes, weights, line-heights. +- The `rounded` section: corner radius values. +- The `spacing` section: scale values. +- The `components` section: token bindings for specific components. + +### 2. Read the screenshots + +Read both screenshots visually. Identify: +- Background colors on each surface level. +- Text colors (primary, muted, disabled). +- Border colors. +- Spacing between elements. +- Corner radii on cards, inputs, buttons. +- Font family and approximate weights. +- Whether dark mode inverts as expected. + +### 3. Check the component source (optional but preferred) + +If the component source is accessible, read it to confirm token usage: + +```bash +find /Users/anilcan/Code/fluttersdk/magic_starter/lib/src/ui/components -name "*.dart" | xargs grep -l "" +``` + +Look for raw `Color(0xFF...)`, `Colors.*`, or hardcoded pixel margins that indicate a token bypass. + +```bash +grep -rn "Color(0x\|Colors\." /Users/anilcan/Code/fluttersdk/magic_starter/lib/src/ui/components// +grep -rn "SizedBox(height: [0-9]\|SizedBox(width: [0-9]" /Users/anilcan/Code/fluttersdk/magic_starter/lib/src/ui/components// +``` + +--- + +## SCORING DIMENSIONS + +Evaluate across five dimensions: + +### 1. Token Compliance (BLOCKING) + +- Background colors match `DESIGN.md` `colors` section (light and dark hex). +- Text colors match `fg`, `fg-muted`, `fg-disabled` roles. +- Border colors match `border` or `border-subtle` roles. +- Interactive element colors match `primary`, `on-primary`, `destructive`, etc. +- No raw hex or `Colors.*` visible in source code for this component. + +Any token violation is **blocking**: the delta MUST be fixed before shipping. + +### 2. Dark/Light Parity (BLOCKING if missing) + +- Dark mode screenshot is visually distinct from light mode. +- Surfaces that are light in light mode are dark in dark mode (and vice versa). +- Text that is dark in light mode is light in dark mode. +- No element is the same color in both modes (unless it is intentionally neutral, e.g. pure white icons on a brand-colored button). + +If light and dark screenshots look identical, the `dark:` counterpart token is missing. This is blocking. + +### 3. Layout and Spacing (advisory) + +- Spacing between elements matches the 4px scale from `DESIGN.md`. +- Touch targets are at least 44pt/48dp. +- Groups have more space between them than within them. +- Content does not fill the entire width when it needs less. + +### 4. Typography (advisory) + +- Font family is Inter (per DESIGN.md). +- Font sizes approximate the DESIGN.md type scale. +- Heading/body/caption hierarchy is visible. +- Line lengths are comfortable (not running the full screen width on wide layouts). + +### 5. Corner Radii (advisory) + +- Cards and dialogs use `lg` (16px) radius. +- Inputs and small controls use `DEFAULT` (8px) radius. +- Badges and pills use `full` (9999px) radius. +- Buttons use `md` (12px) radius. + +--- + +## OUTPUT FORMAT + +Return a numbered delta list. Mark each item as either `[BLOCKING]` or `[ADVISORY]`. + +``` +Component: +Mode: light + dark pair reviewed + +1. [BLOCKING] Token violation: background: light screenshot shows #F0F0F0 on the card surface; DESIGN.md `surface-container` is #F9FAFB. Check that `bg-surface-container` alias is applied, not a hardcoded palette utility. + +2. [BLOCKING] Dark mode missing: dark screenshot is visually identical to light screenshot. The card background does not invert. The alias `bg-surface-container` may not include its `dark:` counterpart. + +3. [ADVISORY] Spacing: the gap between the label and input (appears ~6px) is below the 8px minimum for related elements. Use `gap-2` (8px) minimum. + +4. [ADVISORY] Typography: caption text appears lighter than `text-fg-muted` tone in light mode; check that `Typography(variant: TypographyVariant.caption)` applies `text-fg-muted` correctly. + +5. [ADVISORY] Touch target: the close icon button appears to be ~32x32dp; add `min-h-11 min-w-11` to meet the 44dp minimum. +``` + +If there are no issues: + +``` +Component: +Mode: light + dark pair reviewed + +No deltas. Token compliance, dark/light parity, spacing, typography, and corner radii all match DESIGN.md. Approved. +``` + +--- + +## BLOCKING POLICY + +If any blocking item exists: +- State it clearly at the top: "BLOCKED: blocking item(s) found." +- The caller MUST fix all blocking items and re-invoke the reviewer before the component is considered ship-ready. +- Do not approve a component with a blocking delta, even if all advisory items are clean. + +--- + +## WHAT YOU DO NOT DO + +- Do not modify source files. +- Do not run the app or trigger hot reloads. +- Do not approve your own output (you are always reviewing a peer's work). +- Do not score items outside the five dimensions above. +- Do not make aesthetic judgments beyond token compliance (color preferences, layout choices beyond spacing rules, etc. are outside your scope). diff --git a/.claude/rules/design.md b/.claude/rules/design.md new file mode 100644 index 0000000..fea39d4 --- /dev/null +++ b/.claude/rules/design.md @@ -0,0 +1,109 @@ +--- +paths: + - "lib/**" +--- + +# Design Rules (UI surface) + +These rules apply whenever you touch any file under `lib/`. They complement `CLAUDE.md` with the implementation-level specifics. + +## Atomic Component Folder Contract + +Every component in the `magic_starter` generic library lives in a 4-file atomic folder: + +``` +magic_starter/lib/src/ui/components// + .dart # class extends StatelessWidget, @immutable + .recipe.dart # WindRecipe or WindSlotRecipe + .preview.dart # ONE preview widget rendering all variant x state combos + index.dart # exports .dart + .recipe.dart (NOT preview) +``` + +- Folder and file names are `lower_snake_case` (dotted suffixes `.recipe.dart`/`.preview.dart` are valid). +- Component class name is unprefixed `UpperCamelCase` (`card.dart` -> `class Card`). +- `index.dart` exports the component class, variant enums, and the recipe. Never export the preview file (it is dev-only). +- `previews:refresh` discovers components by scanning `*.preview.dart` files. One preview class per file, no exceptions. + +To scaffold: `dart run bin/dispatcher.dart make:component [--variants=intent,size] [--slots]` + +## WindRecipe Usage + +All variant logic lives in a `WindRecipe` (or `WindSlotRecipe` for slot-based components): + +```dart +final myRecipe = WindRecipe( + base: 'flex items-center gap-2', + variants: { + 'intent': { + 'primary': 'bg-primary text-on-primary', + 'secondary': 'bg-surface-container text-fg border border-color-border', + 'ghost': 'text-fg', + }, + 'size': { + 'sm': 'px-3 py-1.5 text-xs', + 'md': 'px-4 py-2 text-sm', + 'lg': 'px-5 py-2.5 text-base', + }, + }, + defaultVariants: {'intent': 'primary', 'size': 'md'}, +); +``` + +- Emission order is always: `base ++ variant (definition order) ++ compound ++ caller`. Never sort or deduplicate. +- Pass variant values as strings matching the map keys. Pass `null` to clear a default. +- The caller `className` argument appends last; it can override variant output at the same granularity. +- Import `WindRecipe` via `package:magic/magic.dart` inside `magic_starter` files (it re-exports the wind barrel). Direct `package:fluttersdk_wind/...` imports trip `depend_on_referenced_packages`. + +## Token-Only Rule + +All colors go through semantic alias keys defined in `DESIGN.md`. Never use raw hex, `Color(0xFF...)`, or `Colors.*` in component or view code. + +The 17 semantic alias keys: + +| Key | Role | +|-----|------| +| `bg-surface` | Page background | +| `bg-surface-container` | Card, panel background | +| `bg-surface-container-high` | Input background, nested panels | +| `text-fg` | Primary text | +| `text-fg-muted` | Secondary text | +| `text-fg-disabled` | Disabled/meta text | +| `bg-primary` | Brand action background | +| `text-on-primary` | Text on brand action surface | +| `bg-primary-container` | Tinted brand surface | +| `bg-accent` | Secondary accent | +| `border-color-border` | Dividers, card borders | +| `border-color-border-subtle` | Hairline borders | +| `bg-destructive` | Danger action background | +| `text-on-destructive` | Text on danger surface | +| `bg-destructive-container` | Tinted danger surface | +| `bg-success` | Success tone | +| `bg-warning` | Warning tone | + +Each alias expands to a `' dark:'` pair. Every `className` that sets a color MUST include the `dark:` counterpart. The alias system handles this automatically when you use the keys above. + +To add or change a semantic token: edit `DESIGN.md` and run `dart run bin/dispatcher.dart design:sync`. + +## Preview-Required Rule + +No component ships without a preview widget. + +- The preview file (`.preview.dart`) must render every variant x state combination so the catalog shows the full range. +- After adding or modifying a preview, regenerate the catalog: `dart run bin/dispatcher.dart previews:refresh` +- Verify dark/light parity by navigating to `/preview` in debug mode: `./bin/fsa dusk:navigate --route=/preview` +- Take light and dark screenshots and run the `component-visual-reviewer` agent (`.claude/agents/component-visual-reviewer.md`) before marking a component ship-ready. + +## Material Import Discipline + +Component files that share a name with a Material widget (`Card`, `Switch`, `Badge`, `Tooltip`, `Checkbox`, etc.) must import Flutter as: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart' show Icons; // only if icons are needed +``` + +Never `import 'package:flutter/material.dart'` without `show`. Build exclusively on Wind W-widgets inside component bodies. + +## Release Boundary + +Preview files (`*.preview.dart`) and the generated `_previews.g.dart` are dev-only. They are excluded from release builds through the `magic_devtools` dev-package boundary and `kDebugMode`/`kReleaseMode` const-fold. Never import a preview file from production code. diff --git a/.claude/skills/design-first-workflow/SKILL.md b/.claude/skills/design-first-workflow/SKILL.md new file mode 100644 index 0000000..ccfdd23 --- /dev/null +++ b/.claude/skills/design-first-workflow/SKILL.md @@ -0,0 +1,207 @@ +--- +name: design-first-workflow +description: "End-to-end design-first loop for building screens and composing components: design token binding -> scaffold -> preview dark/light -> compose -> wire to controller -> dusk verify. Encodes the CREATE->SCREENSHOT->ANALYZE->FIX->VERIFY self-heal cycle with hard limits." +when_to_use: "TRIGGER when: building a new screen, composing existing components into a view, wiring a view to a controller, or running a full design-first verification pass." +--- + +# Design-First Workflow + +The end-to-end loop for building a screen or view in this project: from DESIGN.md token decisions to a dusk-verified, dark/light-parity-confirmed, controller-wired result. + +--- + +## THE LOOP + +``` +DESIGN -> SCAFFOLD -> PREVIEW -> COMPOSE -> WIRE -> VERIFY +``` + +For iterative fixes after the first preview: + +``` +SCREENSHOT -> ANALYZE -> FIX -> VERIFY +maximum 3 rounds; stop if no improvement across a full round +``` + +--- + +## PHASE 1: DESIGN + +Read `magic_example/DESIGN.md` before touching any code. + +Decide: +1. Which semantic tokens govern this screen (background, text, interactive elements). +2. Which components from `docs/component-registry.md` cover the UI needs. +3. The visual hierarchy: what is primary, secondary, tertiary on this screen. +4. Responsive behavior: narrow (single column) vs `md` and wider (sidebar + content, or row layout). + +Do NOT start coding until the token bindings are clear. A screen that uses the right tokens will automatically update when the brand changes; a screen with hardcoded hex will not. + +--- + +## PHASE 2: SCAFFOLD (new screens) + +For a new standalone screen, create the view file: + +```sh +dart run bin/dispatcher.dart make:component Preview [--slots] +``` + +Or author the view file directly if it follows an existing auth/profile/settings pattern. Views live under `magic_starter/lib/src/ui/views//`. Components live under `magic_starter/lib/src/ui/components//`. + +For a new component within the screen, follow the `make-component` skill. + +--- + +## PHASE 3: PREVIEW (dark + light) + +Every screen must be previewed in both light and dark before wiring to a controller. + +Start the app and navigate to the preview catalog: + +```sh +./bin/fsa start --device=chrome +./bin/fsa dusk:navigate --route=/preview +``` + +Capture both modes: + +```sh +# Light mode +./bin/fsa dusk:screenshot -o /tmp/-light.jpg + +# Toggle to dark mode via the catalog header toggle, then: +./bin/fsa dusk:screenshot -o /tmp/-dark.jpg +``` + +Dark and light screenshots MUST be visually distinct. If they look identical, a semantic token alias is missing its `dark:` counterpart. + +--- + +## PHASE 4: COMPOSE + +Compose the screen from library components. Rules: + +- Use `magic_starter` components from `docs/component-registry.md`. Do not write one-off inline widgets when a library component fits. +- Token discipline: `bg-surface`, `text-fg`, `border-color-border`, etc. Never `Colors.grey.shade200`. +- Layout: `WDiv` with `flex-col` or `flex-row`; Wind breakpoint prefixes for responsive behavior. +- Interactive elements: `Button`, `Input`, `Checkbox`, `Switch` from the component library; not raw `WButton`/`WInput` unless the component library explicitly wraps them. + +--- + +## PHASE 5: WIRE TO CONTROLLER + +Wire the screen to its `MagicStatefulView`: + +1. The view is a `MagicStatefulView` or `MagicView`. +2. The controller extends `MagicController` + `MagicStateMixin`. +3. All data flows through the controller; the view reads `controller.state` and calls controller methods. +4. API calls use `Http.post/get/put/delete`; error handling via `handleApiError`. +5. Gate-checked UI elements use `Auth.can('ability')` (advisory; backend enforces). + +Preserve the view-registry key (`'auth.login'`, `'profile.settings'`, etc.) unchanged. + +--- + +## PHASE 6: VERIFY (self-heal loop) + +Run the full self-heal cycle after wiring: + +### CREATE + +Navigate to the screen in the running app: + +```sh +./bin/fsa dusk:navigate --route=/ +``` + +Or use the `/preview` catalog for isolated component testing. + +### SCREENSHOT + +Capture screenshots for both modes: + +```sh +./bin/fsa dusk:screenshot -o /tmp/-light.jpg +# toggle dark, then: +./bin/fsa dusk:screenshot -o /tmp/-dark.jpg +``` + +### ANALYZE + +Invoke the `component-visual-reviewer` subagent: + +``` +Agent({subagent_type: "ac:component-visual-reviewer"}) with: + - screenshot_light: /tmp/-light.jpg + - screenshot_dark: /tmp/-dark.jpg + - design_md: magic_example/DESIGN.md + - component: +``` + +The reviewer returns: +- A numbered delta list (layout, spacing, token compliance, dark/light parity, typography). +- **Blocking** items: token violations (wrong color, missing dark variant, raw hex visible in screenshot). These must be fixed before proceeding. +- **Advisory** items: spacing, layout, typography notes. Fix if straightforward; log the rest. + +### FIX + +Address each blocking delta: +- Token violation: replace the non-token value with the correct semantic alias. +- Missing dark counterpart: add `dark:` class to the alias (or check that `design:sync` generated the alias correctly). +- Wrong component: swap to the correct library component. + +After fixing, regenerate if needed: + +```sh +dart run bin/dispatcher.dart previews:refresh +./bin/fsa reload +``` + +### VERIFY + +Re-screenshot and re-analyze. Repeat the cycle. + +**Stop conditions:** +- All blocking items are resolved. +- No improvement across a full round (three rounds maximum; escalate if still failing after round 3). + +--- + +## RULES + +- Read `DESIGN.md` before any token decision. +- Read `docs/component-registry.md` before building a component. +- Preview dark + light before wiring to a controller. +- Maximum 3 self-heal rounds per screen or component. +- Stop on no-improvement: if the delta list does not shrink after a round, escalate rather than looping. +- Token violations are always blocking; layout/spacing notes are advisory. +- `make:component` + `previews:refresh` are the only ways to add components to the catalog. Do not manually edit `_previews.g.dart`. +- Do not wire a screen to a real backend during preview; use mock controllers or `Http.fake()` stubs. + +--- + +## COMMANDS REFERENCE + +| Command | Phase | What it does | +|---------|-------|-------------| +| `dart run bin/dispatcher.dart make:component ` | SCAFFOLD | Scaffold atomic folder + chain previews:refresh | +| `dart run bin/dispatcher.dart previews:refresh` | SCAFFOLD/FIX | Regenerate preview registry | +| `dart run bin/dispatcher.dart design:sync` | DESIGN | Regenerate Wind theme from DESIGN.md | +| `dart run bin/dispatcher.dart design:lint` | DESIGN | Validate DESIGN.md | +| `./bin/fsa start --device=chrome` | PREVIEW | Boot app for dusk interaction | +| `./bin/fsa dusk:navigate --route=/preview` | PREVIEW | Open preview catalog | +| `./bin/fsa dusk:screenshot -o ` | SCREENSHOT | Capture current app state | +| `./bin/fsa reload` | FIX | Hot reload after changes | + +--- + +## REFERENCES + +- `magic_example/DESIGN.md` token source +- `docs/component-registry.md` component inventory +- `.claude/skills/frontend-design/SKILL.md` token/color/type/spacing guidance +- `.claude/skills/make-component/SKILL.md` component scaffold detail +- `.claude/agents/component-visual-reviewer.md` visual review subagent +- `docs/design-culture/refactoring-ui.md` hierarchy and spacing principles +- `docs/design-culture/wind-responsive.md` breakpoints and layout patterns diff --git a/.claude/skills/frontend-design/SKILL.md b/.claude/skills/frontend-design/SKILL.md new file mode 100644 index 0000000..55828d1 --- /dev/null +++ b/.claude/skills/frontend-design/SKILL.md @@ -0,0 +1,357 @@ +--- +name: frontend-design +description: "Flutter/Wind/Magic-native UI design skill: design systems, visual hierarchy, bold aesthetics, and semantic token usage for this project. Covers component authoring, DESIGN.md-driven theming, and dark/light parity. Use for any UI, component, or screen work in magic_example." +when_to_use: "TRIGGER when: UI, pages, components, screens, design. DO NOT TRIGGER when: backend, auth logic, or non-visual." +--- + +# Frontend Design (Flutter/Wind/Magic) + +Production-grade UI design for Flutter apps built on the Wind utility system. This is a project-specific fork of the generic frontend-design skill, pinned to MOBILE/Flutter mode. All guidance is Wind className and WindRecipe based; web/CSS-only directives have been reconciled or removed. + +--- + +## MODE + +This skill is **MOBILE/Flutter only.** There is no web/CSS mode for this project. + +Platform: Flutter (mobile-first, responsive via Wind breakpoint prefixes). +Styling: Wind className strings only. No raw `Colors.*`, no hardcoded hex in component code. +Theme: `DESIGN.md` is the single source of truth for all tokens. See the `colors`, `typography`, `rounded`, and `spacing` sections. +Components: use the project component library (`magic_starter` generic component set). Never build inline one-offs when a library component covers the case. + +--- + +## DESIGN PROCESS (BEFORE CODING) + +Before writing any widget code, commit to a bold aesthetic direction by answering four questions: + +1. **Purpose**: What problem does this screen or component solve? Who uses it? +2. **Tone**: Choose a clear direction and commit fully (brutally minimal, refined precision, warm/approachable, editorial, etc.). +3. **Constraints**: Touch targets, safe areas, responsive breakpoints (`sm`/`md`/`lg` via Wind), accessibility (4.5:1 WCAG AA). +4. **Differentiation**: What is the one thing a user will remember about this surface? + +Then implement working code that is production-grade, visually distinctive, and cohesive. + +### Design-First Workflow + +1. Design the actual piece of functionality first, not the navigation shell. +2. Work in grayscale first; add color after hierarchy is clear. +3. Establish token bindings (spacing, type, color via semantic aliases) before detailed styling. +4. Iterate in cycles; details come last. + +For the full workflow loop (including screenshot + verify), see the `design-first-workflow` skill. + +--- + +## DESIGN SYSTEMS + +### Spacing Scale + +Defer to `DESIGN.md`'s `spacing` section and the Wind utility scale. The project 4px logical scale: + +| Token | Size | Wind class | Use case | +|-------|------|-----------|----------| +| xs | 4px | `p-1` / `gap-1` | Micro gaps, icon padding | +| sm | 8px | `p-2` / `gap-2` | Within components | +| md | 16px | `p-4` / `gap-4` | Standard screen padding | +| lg | 24px | `p-6` / `gap-6` | Between sections | +| xl | 40px | `p-10` / `gap-10` | Major separation | +| 2xl | 64px | `p-16` / `gap-16` | Hero areas | +| gutter | 16px | `px-4` | Horizontal content margin (narrow screens) | +| section | 32px | `py-8` | Stacked section separation | + +Do not use arbitrary pixel values (`p-[13px]`); stay on the 4px scale. For semantic spacing tokens, use the alias keys from `magic_example/DESIGN.md`. + +### Type Scale + +Typography is **Inter** (the authoritative font from `DESIGN.md`). Use the `Typography` component or Wind text utilities matching the DESIGN.md scale. All sizes are logical pixels. + +| DESIGN.md token | Wind approx | Role | +|----------------|-------------|------| +| `label-sm` | `text-xs` | Captions, meta, timestamps | +| `body-md` | `text-sm` | Default body text | +| `body-lg` | `text-base` | Emphasized body | +| `title-lg` | `text-lg` | Card/section titles | +| `headline-md` | `text-xl` | Subheadings | +| `headline-lg` | `text-2xl` | Screen titles | +| `display` | `text-3xl` | Hero/display text | + +Line-height and letter-spacing from DESIGN.md apply; they are already encoded in the `Typography` component recipe. + +**Font selection for this project**: Inter is the authoritative app font per `DESIGN.md`. The generic fork's "never Inter/Roboto/system-ui" rule does NOT apply here. DESIGN.md typography is always authoritative over general font guidance. + +### Shadow and Elevation + +Wind does not support CSS `box-shadow` or `filter` utilities (part of the ~72 unsupported CSS families). Express depth through: + +- Background tonal shifts: `bg-surface` -> `bg-surface-container` -> `bg-surface-container-high` +- Subtle border lines: `border border-color-border` +- Use the `WindRecipe` `shadow-sm`/`shadow-md` tokens only if the consumer WindThemeData has them aliased; otherwise rely on tonal backgrounds. + +For elevation semantics, see [docs/design-culture/material-design-3.md](../../docs/design-culture/material-design-3.md). + +### Transforms and Filters + +Wind does not support CSS `transform`, `rotate`, `scale`, `translate`, `filter`, `backdrop-filter`, `group-*`, or `peer-*` utilities. For motion and transitions, use Flutter's animation system directly (`AnimatedContainer`, `AnimatedOpacity`, `TweenAnimationBuilder`), not Wind className strings. + +--- + +## VISUAL HIERARCHY + +Every element sits at one of three levels: + +- **Primary**: `text-fg` + heavy weight (`font-bold`) headlines, key actions (one per section) +- **Secondary**: `text-fg-muted` supporting text, dates, descriptions +- **Tertiary**: `text-fg-disabled` metadata, timestamps, copyright + +### Key Principles + +- Size is not everything: use weight and color before increasing font size. +- **Emphasize by de-emphasizing**: soften competing elements instead of loudening the target. +- Labels are a last resort: combine with values ("12 left in stock" beats "Stock: 12"). +- Icons are visually heavy: give them `text-fg-muted` or `text-fg-disabled` to balance with text. + +### Button Hierarchy + +| Level | Wind recipe | Rule | +|-------|-------------|------| +| Primary | `Button(intent: ButtonIntent.primary)` | One per section maximum | +| Secondary | `Button(intent: ButtonIntent.secondary)` | Clear but not competing | +| Ghost | `Button(intent: ButtonIntent.ghost)` | Discoverable, unobtrusive | +| Destructive | `Button(intent: ButtonIntent.destructive)` | Only on destructive actions | + +Destructive actions do not have to be big/red/bold on all screens. On regular content pages where delete is secondary, use ghost or secondary styling. Reserve full destructive styling for confirmation dialogs. + +--- + +## COLOR SYSTEM + +### Use Semantic Tokens, Not Hex + +All color decisions go through semantic alias tokens defined in `DESIGN.md`. Never put raw hex or `Colors.*` in component code. + +| Role | Wind alias | Use | +|------|-----------|-----| +| `bg-surface` | Page background | | +| `bg-surface-container` | Card, panel background | | +| `bg-surface-container-high` | Input background, nested panels | | +| `text-fg` | Primary text | | +| `text-fg-muted` | Secondary text | | +| `text-fg-disabled` | Disabled/meta text | | +| `bg-primary` / `text-on-primary` | Brand action / on-brand text | | +| `bg-primary-container` | Tinted brand surface | | +| `bg-accent` | Secondary accent | | +| `border-color-border` | Dividers, card borders | | +| `border-color-border-subtle` | Hairline borders | | +| `bg-destructive` / `text-on-destructive` | Danger action / on-danger text | | +| `bg-success` / `bg-warning` | Status tones | | + +For arbitrary-hex aliases generated by `design:sync`, Wind expands them as arbitrary-value utilities: `bg-[#7C3AED] dark:bg-[#8B5CF6]`. + +### Dark/Light Parity + +Every Wind className that carries a color token MUST include its `dark:` counterpart. This is enforced by the alias system: each alias key expands to a light+dark pair (e.g. `bg-surface` -> `bg-white dark:bg-[#030712]`). Never set a background or text color without a `dark:` override. + +Use the `/preview` catalog and the `component-visual-reviewer` subagent to verify dark/light parity before shipping. + +### Accessibility + +| Text type | Minimum contrast | Checked by | +|-----------|-----------------|-----------| +| Normal text (<18px) | 4.5:1 | `design:lint` | +| Large text (18px+ bold or 24px+) | 3:1 | `design:lint` | + +Never rely on color alone for meaning. Add icons, text, or patterns alongside color cues. + +--- + +## TYPOGRAPHY + +Inter is the project font (see `DESIGN.md` typography section). Use the `Typography` component for all text rendering rather than raw `WText` with ad-hoc sizes. + +### Line-Height and Spacing + +- Small text: taller line-height (1.5-2.0 equivalent). +- Large headlines: shorter line-height (1.0-1.2 equivalent). +- These are already encoded in the DESIGN.md `typography` entries; use them as-is. + +### Alignment + +- Default: left-aligned. +- Center: only for headlines and short blocks (under 2-3 lines). +- Right-align numbers in column comparison contexts. + +--- + +## LAYOUT AND SPACING + +### Mobile-First + +Design for narrow screens first. Use Wind breakpoint prefixes to expand at `sm` (640px) and `md` (768px): + +``` +// narrow: stacked columns +// md and wider: row layout +WDiv(className: 'flex flex-col md:flex-row gap-4') +``` + +### Touch Targets + +| Minimum | Comfortable | +|---------|------------| +| 44x44 pt (iOS) | 48x48 dp (Android) | + +Add invisible padding if an icon or label is smaller than the minimum target. Use `min-h-11 min-w-11` as a floor. + +### Safe Areas + +Respect device notches and home indicators via Flutter's `SafeArea` widget. Never place interactive elements in unsafe areas. + +### Navigation Patterns + +| Pattern | Wind/Magic component | Use case | +|---------|---------------------|---------| +| Bottom navigation | `Navbar` | 3-5 primary destinations | +| Tab bar | `Tabs` | Content categories | +| Navigation drawer | `AppLayout` sidebar | Many destinations | +| Bottom sheet | `BottomSheet` | Contextual actions | + +### Spacing Discipline + +More space between groups than within groups: +- Form labels sit closer to their input than to the preceding element. +- Section headings have more space above than below. +- List items within a group are tighter than the group gap. + +--- + +## DEPTH AND MOTION + +### Tonal Depth (Wind-compatible) + +Wind does not support `box-shadow` or `filter`. Express depth through tonal backgrounds: + +- Raised: use a lighter background (`bg-surface-container`) against the page (`bg-surface`). +- Inset: use a darker/deeper background (`bg-surface-container-high`) for inputs and nested panels. + +### Motion + +Flutter animation system handles motion; Wind className strings do not carry transitions/transforms. Focus on high-impact moments: + +- Page transitions: use `MagicRoute` transition settings, not custom animations per-view. +- State changes (loading/error): use the `Skeleton` component for loading states; avoid inline spinners. +- Reduced motion: respect `MediaQuery.of(context).disableAnimations` in all custom animations. + +For detailed easing/duration guidance, see [docs/design-culture/motion-interaction.md](../../docs/design-culture/motion-interaction.md). + +--- + +## SPATIAL COMPOSITION + +- Asymmetry and unexpected layouts can add character; do not default to symmetric grids. +- Generous negative space reads as premium; controlled density reads as rich/capable. +- Avoid filling the whole screen when the content only needs part of it. + +--- + +## MOBILE-SPECIFIC PATTERNS + +### Loading States + +Use the `Skeleton` component (preferred over spinners). Shape variants: `block`, `text`, `circle`. + +### Empty States + +Use the `EmptyState` component: +- Illustration or icon to grab attention. +- Clear title and helpful description. +- A call-to-action `Button`. +- Hide irrelevant UI (filters, tabs) when they have no effect yet. + +### Forms + +- Full-width `Input` with horizontal `gutter` padding. +- `FormField` handles label + hint + error state (never roll inline). +- Primary action: full-width `Button(intent: ButtonIntent.primary)` at the bottom. +- Error state: `border-color-border` turns `border-color-destructive`; use `FormField`'s error slot. + +--- + +## ANTI-PATTERNS + +| Anti-pattern | Fix | +|-------------|-----| +| Raw `Color(0xFF...)` or `Colors.*` in component code | Use Wind semantic token alias | +| Hardcoded pixel values (`SizedBox(height: 13)`) | Use Wind spacing utilities on the 4px scale | +| Font family chosen outside DESIGN.md | DESIGN.md typography is authoritative; Inter is the font | +| Purple gradients on white without dark counterpart | Every color token needs its `dark:` pair | +| Filling the whole screen when content needs less | Add `max-w-*` or `mx-auto`; let content breathe | +| Ambiguous spacing between groups | More space between groups than within | +| Touch targets under 44pt/48dp | Add `min-h-11 min-w-11` or invisible padding | +| Ignoring safe areas | Wrap top-level screens in `SafeArea` | +| Color as sole communication channel | Add icon, text, or pattern alongside color | +| Multiple previews in one file | One preview class per `*.preview.dart` file | +| CSS-only utilities (`box-shadow`, `filter`, `transform`, `group-*`) | These are unsupported by Wind; use Flutter APIs instead | +| `Icons.*` inline in component bodies | Extract as `static const IconData _icon = Icons.x;` | +| Skipping dark/light parity | Every semantic token alias is a light+dark pair; no exceptions | +| Building one-off widgets when a library component exists | Always check `magic_starter` component library first | + +--- + +## COMPONENT AUTHORING + +When building a new component, follow the atomic folder convention: + +``` +magic_starter/lib/src/ui/components// + .dart # class extends StatelessWidget + .recipe.dart # WindRecipe / WindSlotRecipe + .preview.dart # single preview widget (ONE per file) + index.dart # barrel: export .dart + .recipe.dart (NOT preview) +``` + +Generate the scaffold with: + +```sh +dart run bin/dispatcher.dart make:component [--variants=intent,size] [--slots] +``` + +This chains `previews:refresh` automatically. + +Every new component needs: +1. A `WindRecipe` or `WindSlotRecipe` in the recipe file. +2. Semantic token classNames only (from `DESIGN.md` alias set). +3. A preview widget rendering every variant x state combination. +4. Tests asserting the rendered `WDiv.className` for each variant. + +For the full component lifecycle (design -> scaffold -> preview -> verify), load the `make-component` skill. + +--- + +## OUTPUT GUIDANCE + +When generating UI code, structure output as: + +1. **Design direction** 1-2 sentences on aesthetic approach and key token decisions. +2. **Code** complete, working Flutter/Wind implementation. +3. **Responsive notes** breakpoint behavior if applicable (narrow -> `md` -> wider). + +Lead with code, not explanation. + +--- + +## REFERENCES + +| Topic | File | +|-------|------| +| DESIGN.md (token source) | `magic_example/DESIGN.md` | +| Visual hierarchy | `docs/design-culture/refactoring-ui.md` | +| Color system + contrast | `docs/design-culture/accessibility-wcag.md` | +| Mobile patterns + safe areas | `docs/design-culture/wind-responsive.md` | +| Material 3 role semantics | `docs/design-culture/material-design-3.md` | +| Motion + easing | `docs/design-culture/motion-interaction.md` | +| Apple HIG / iOS patterns | `docs/design-culture/apple-hig.md` | +| Component authoring workflow | `.claude/skills/make-component/SKILL.md` | +| Design-first end-to-end loop | `.claude/skills/design-first-workflow/SKILL.md` | +| Visual reviewer subagent | `.claude/agents/component-visual-reviewer.md` | +| Component registry | `docs/component-registry.md` | diff --git a/.claude/skills/make-component/SKILL.md b/.claude/skills/make-component/SKILL.md new file mode 100644 index 0000000..11ad12c --- /dev/null +++ b/.claude/skills/make-component/SKILL.md @@ -0,0 +1,198 @@ +--- +name: make-component +description: "Scaffold, preview, and verify a new magic_starter component using the design->scaffold->preview->verify loop. Covers make:component, previews:refresh, /preview catalog navigation, dusk screenshot capture, and the component-visual-reviewer sign-off." +when_to_use: "TRIGGER when: creating a new component, adding a variant to an existing component, or verifying a component's visual output." +--- + +# Make Component + +The full lifecycle for authoring a component in this project: from design intent to a reviewed, catalog-visible, token-compliant widget. + +--- + +## OVERVIEW + +Every component follows the atomic 4-file folder shape: + +``` +magic_starter/lib/src/ui/components// + .dart # class extends StatelessWidget, @immutable + .recipe.dart # WindRecipe or WindSlotRecipe + .preview.dart # ONE preview widget rendering all variants + index.dart # exports .dart + .recipe.dart; NOT the preview +``` + +The lifecycle is: **DESIGN -> SCAFFOLD -> PREVIEW -> VERIFY**. + +--- + +## STEP 1: DESIGN + +Before scaffolding, decide: + +- **Component name** (PascalCase, no prefix): `Button`, `Card`, `Badge`, etc. +- **Variant axes** (intent, size, tone, shape, etc.) and their values. +- **Slots** (if the component composes named child regions: `trigger`, `panel`, `header`, `footer`). +- **Token bindings**: which semantic alias keys (`bg-primary`, `text-fg`, `border-color-border`, etc.) map to which visual role. + +Consult `magic_example/DESIGN.md` for the authoritative token set. Consult `docs/component-registry.md` to avoid duplicating an existing component. + +--- + +## STEP 2: SCAFFOLD + +Generate the 4-file folder with: + +```sh +dart run bin/dispatcher.dart make:component [--variants=intent,size] [--slots] +``` + +This command: +1. Creates `magic_starter/lib/src/ui/components//` with the 4 files. +2. Populates the recipe with the requested variant axes. +3. Automatically chains `previews:refresh` so the new preview is registered. + +After scaffolding, edit the generated files: + +- `.recipe.dart`: fill in token classNames for each variant value. Use semantic aliases only (`bg-primary`, `text-on-primary`, `bg-surface-container`, etc.). +- `.dart`: build the widget body using Wind W-widgets (`WDiv`, `WText`, `WButton`, etc.). Import via `package:magic/magic.dart` (re-exports the full wind barrel). +- `.preview.dart`: render a variant matrix (every combination of variant axes + dark/light). Keep ONE preview class per file. +- `index.dart`: verify the exports include the component class and any variant enums, but NOT the preview class. + +If you add the component to the barrel, export it from `magic_starter/lib/magic_starter.dart`: + +```sh +# After editing, regenerate the preview registry: +dart run bin/dispatcher.dart previews:refresh +``` + +--- + +## STEP 3: PREVIEW + +Open the preview catalog to inspect the component visually: + +```sh +# In one terminal: start the app +./bin/fsa start --device=chrome + +# In another terminal: navigate to the preview catalog +./bin/fsa dusk:navigate --route=/preview +``` + +The `/preview` catalog lists all registered previews. Each `*.preview.dart` produces one entry. The catalog provides a global dark/light toggle to check both modes. + +If the component is not listed, run `previews:refresh` again and hot-reload: + +```sh +dart run bin/dispatcher.dart previews:refresh +./bin/fsa reload +``` + +--- + +## STEP 4: SCREENSHOT + +Capture screenshots for visual review: + +```sh +# Light mode screenshot +./bin/fsa dusk:screenshot -o /tmp/-light.jpg + +# Toggle to dark (tap the theme toggle in the catalog header), then: +./bin/fsa dusk:screenshot -o /tmp/-dark.jpg +``` + +Both files must be non-empty and visually distinct (dark != light). + +--- + +## STEP 5: VERIFY (component-visual-reviewer) + +Invoke the `component-visual-reviewer` subagent to score the screenshot pair against `DESIGN.md` tokens: + +``` +Agent({subagent_type: "ac:component-visual-reviewer"}) with: + - screenshot_light: /tmp/-light.jpg + - screenshot_dark: /tmp/-dark.jpg + - design_md: magic_example/DESIGN.md + - component: +``` + +The reviewer returns a numbered delta list. Any token violation (wrong color, missing dark variant, hardcoded hex visible) is blocking. Layout and spacing deltas are advisory. + +Fix each blocking delta, re-run `previews:refresh`, hot-reload, re-screenshot, and re-verify. Maximum 3 rounds; stop if no improvement across a full round. + +--- + +## RECIPE AUTHORING RULES + +- Emission order is `base ++ variant(definition order) ++ compound(array order) ++ caller`. Never change this. +- Override at the same token granularity: use `px-4` to override `px-*`, not `p-*` overriding `px-*`. +- `defaultVariants` sets the fallback when the caller does not specify an axis. +- Pass enum values as `.name` strings: `ButtonIntent.primary.name` -> `'primary'`. +- Compound variants fire when multiple axes match simultaneously. +- The `className` override parameter allows the caller to bypass the recipe entirely (escape hatch; keep it). + +Example WindRecipe shape: + +```dart +final buttonRecipe = WindRecipe( + base: 'inline-flex items-center justify-center font-semibold rounded-md transition-colors', + variants: { + 'intent': { + 'primary': 'bg-primary text-on-primary', + 'secondary': 'bg-surface-container text-fg border border-color-border', + 'ghost': 'bg-transparent text-fg-muted', + 'destructive': 'bg-destructive text-on-destructive', + }, + 'size': { + 'sm': 'text-xs px-3 py-1.5', + 'md': 'text-sm px-4 py-2', + 'lg': 'text-base px-6 py-3', + }, + }, + defaultVariants: {'intent': 'primary', 'size': 'md'}, +); +``` + +--- + +## MIGRATING AN EXISTING WIDGET + +If the component replaces an existing `MagicStarter*` widget: + +1. Grep the codebase for all callers and existing className-asserting tests. +2. Write a baseline helper (`legacyXClassName(...)`) asserting the recipe output is byte-identical to the old pinned strings BEFORE changing the widget body (TDD red phase). +3. After the recipe is green, move the widget body to call the recipe. +4. Keep the old widget name as a thin re-export alias so callers and tests remain untouched: + +```dart +// magic_starter_button.dart (old location) +export 'components/button/button.dart' show Button, ButtonIntent; +``` + +Never weaken existing assertions to make them pass. + +--- + +## COMMANDS REFERENCE + +| Command | What it does | +|---------|-------------| +| `dart run bin/dispatcher.dart make:component [--variants=a,b] [--slots]` | Scaffold 4-file atomic folder + chain previews:refresh | +| `dart run bin/dispatcher.dart previews:refresh` | Regenerate `_previews.g.dart` from all `*.preview.dart` files | +| `dart run bin/dispatcher.dart design:sync` | Regenerate Wind theme from `DESIGN.md` | +| `dart run bin/dispatcher.dart design:lint` | Validate `DESIGN.md` against design rules | +| `./bin/fsa dusk:navigate --route=/preview` | Open the preview catalog in the running app | +| `./bin/fsa dusk:screenshot -o ` | Capture a screenshot of the running app | + +--- + +## REFERENCES + +- `magic_example/DESIGN.md` token source +- `docs/component-registry.md` component inventory and anti-patterns +- `.claude/skills/frontend-design/SKILL.md` token/color/type guidance +- `.claude/skills/design-first-workflow/SKILL.md` end-to-end loop for composing screens +- `.claude/agents/component-visual-reviewer.md` visual review subagent diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e2c21dc --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +APP_NAME= +APP_ENV= +APP_DEBUG= +APP_KEY= + +API_URL= diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..cf4b47a --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: android + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: ios + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: linux + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: macos + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: web + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: windows + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..97ff2b2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,92 @@ +# Agent Guidance + +Behavioral contract for AI agents (Claude, Cursor, Copilot, etc.) working in this project. + +## Before Any UI Work + +1. Read `DESIGN.md` (loaded automatically via `@DESIGN.md` in `CLAUDE.md`). +2. Read the relevant `docs/design-culture/` file for the surface you are building (loaded automatically via `@docs/design-culture/`). +3. Check `docs/component-registry.md` to see whether a component already exists for the UI need. +4. Load `.claude/skills/frontend-design/SKILL.md` for all component and screen work. + +Do not write a single widget until token bindings are decided. A screen built on the right semantic tokens will adapt to any brand change automatically; a screen with hardcoded hex will not. + +## Visual Self-Heal Loop + +For every new component or screen, run this loop after the first implementation: + +``` +CREATE -> SCREENSHOT -> ANALYZE -> FIX -> VERIFY +``` + +**Maximum 3 rounds.** Stop if no improvement is observed across a full round. + +### Steps + +**CREATE**: implement the component or screen using semantic tokens and library components. + +**SCREENSHOT**: capture light and dark screenshots. + +```sh +# Navigate to the component in the preview catalog +./bin/fsa dusk:navigate --route=/preview + +# Light mode screenshot +./bin/fsa dusk:screenshot -o .ac/evidence/-light.png + +# Switch to dark mode in the preview catalog, then: +./bin/fsa dusk:screenshot -o .ac/evidence/-dark.png +``` + +**ANALYZE**: invoke the `component-visual-reviewer` subagent (`.claude/agents/component-visual-reviewer.md`) with both screenshots and the component name. The reviewer scores token compliance, dark/light parity, spacing, typography, and corner radii. + +**FIX**: address every BLOCKING item from the reviewer. ADVISORY items are addressed if they can be fixed without scope creep. + +**VERIFY**: re-screenshot and re-run the reviewer. If all BLOCKING items are resolved, the component is ship-ready. + +If the reviewer returns the same BLOCKING items after 3 rounds with no progress, stop and surface the issue to the user. + +## Component Registry + +Before scaffolding, always check `docs/component-registry.md`. It maps every available component to its variants, token bindings, and anti-patterns. If the registry has a component that covers the need, use it. + +For new components: + +```sh +dart run bin/dispatcher.dart make:component [--variants=intent,size] [--slots] +``` + +This scaffolds the 4-file atomic folder under `magic_starter/lib/src/ui/components//` and chains `previews:refresh`. + +## Skills to Load + +| Situation | Skill | +|-----------|-------| +| Any UI, component, or screen work | `.claude/skills/frontend-design/SKILL.md` | +| Scaffolding a new component | `.claude/skills/make-component/SKILL.md` | +| Full screen from design to controller-wired | `.claude/skills/design-first-workflow/SKILL.md` | + +## Anti-Patterns + +The following are hard blockers. The `component-visual-reviewer` will flag every one of them. + +| Anti-pattern | Correct approach | +|-------------|-----------------| +| `Color(0xFF...)` or `Colors.*` in component code | Use a semantic alias key from `DESIGN.md` | +| Hardcoded pixel values (`SizedBox(height: 13)`) | Wind spacing utilities on the 4px scale | +| A color token without its `dark:` counterpart | Every alias expands to a light+dark pair; no exceptions | +| Building a one-off widget when a library component exists | Check `docs/component-registry.md` first | +| Multiple preview classes in one `.preview.dart` file | One preview class per file | +| `Icons.*` inline in component body | Extract as `static const IconData _icon = Icons.x;` | +| CSS-only Wind utilities (`box-shadow`, `filter`, `transform`, `group-*`) | These are unsupported; use Flutter animation APIs instead | +| Hand-editing `lib/config/wind_theme.g.dart` | Run `dart run bin/dispatcher.dart design:sync` instead | +| Skipping the preview before shipping a component | Every component needs a preview; run `previews:refresh` | + +## Regeneration Commands + +| Need | Command | +|------|---------| +| Regenerate wind theme from DESIGN.md | `dart run bin/dispatcher.dart design:sync` | +| Regenerate preview catalog | `dart run bin/dispatcher.dart previews:refresh` | +| Lint DESIGN.md | `dart run bin/dispatcher.dart design:lint` | +| Scaffold component | `dart run bin/dispatcher.dart make:component ` | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7276eaf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# Magic Example + +Reference app for the `magic` framework and `magic_starter` starter kit. Single-brand violet, Wind semantic tokens, M3-role palette. Consumes and demonstrates the full design-first component system. + +## Stack + +- Flutter >=3.27.0, Dart >=3.6.0 +- `fluttersdk_magic` (framework: IoC, ORM, auth, routing via `go_router`) +- `magic_starter` (starter kit: auth, profile, teams, notifications, 13 opt-in features) +- `fluttersdk_wind` (utility-first styling: Wind className strings only) +- `magic_devtools` (dev-only preview catalog and dusk integration) + +## Design-First Rules + +These rules apply to every UI file. There are no exceptions. + +**1. Wind className and semantic tokens only.** +No `Color(0xFF...)`, no `Colors.*`, no hardcoded pixel values in component or view code. All color decisions go through semantic alias keys from `DESIGN.md` (e.g. `bg-surface`, `text-fg`, `border-color-border`). Every color alias expands to a light+dark pair; always include the `dark:` counterpart. + +**2. Build from the component library.** +Check `docs/component-registry.md` before writing any widget. If a component covers the need, use it. Only scaffold a new component when the registry has no match. + +**3. DESIGN.md is the theme source.** +`magic_example/DESIGN.md` is the single source of truth for colors, typography, spacing, and rounded values. The generated theme lives at `lib/config/wind_theme.g.dart` (do not hand-edit it). Regenerate after any DESIGN.md change with `dart run bin/dispatcher.dart design:sync`. + +**4. The `/preview` catalog is the visual feedback loop.** +Every new component requires a preview widget before it ships. Navigate to `/preview` in debug mode to see all registered previews in dark and light. Use dusk screenshots to verify token compliance before merging. + +## Commands + +All commands run via the dispatcher. Do not reference `magic_example:artisan` (it does not exist). + +```sh +# Scaffold a new component (chains previews:refresh automatically) +dart run bin/dispatcher.dart make:component [--variants=intent,size] [--slots] + +# Regenerate the wind theme from DESIGN.md +dart run bin/dispatcher.dart design:sync + +# Regenerate lib/preview/_previews.g.dart from *.preview.dart files +dart run bin/dispatcher.dart previews:refresh + +# Lint DESIGN.md token rules (13 checks) +dart run bin/dispatcher.dart design:lint + +# Navigate to /preview in the running app +./bin/fsa dusk:navigate --route=/preview + +# Screenshot the current screen (light or dark) +./bin/fsa dusk:screenshot -o .ac/evidence/-light.png + +# Analyze and format +flutter analyze +dart format . +flutter test +``` + +## Agent Infra + +- Skills: `.claude/skills/frontend-design/`, `.claude/skills/make-component/`, `.claude/skills/design-first-workflow/` +- Agents: `.claude/agents/component-visual-reviewer.md` +- Design culture docs: `docs/design-culture/` (apple-hig, material-design-3, refactoring-ui, accessibility-wcag, motion-interaction, wind-responsive) +- Component registry: `docs/component-registry.md` + +@DESIGN.md +@docs/design-culture/ diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..a0db345 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,225 @@ +--- +name: Magic Example +description: > + Reference app for the magic framework, magic_starter, and the design-first + component system. Single-brand violet, Wind semantic tokens, M3-role palette. +colors: + surface: + light: "#FFFFFF" + dark: "#030712" + surface-container: + light: "#F9FAFB" + dark: "#111827" + surface-container-high: + light: "#F3F4F6" + dark: "#1F2937" + fg: + light: "#111827" + dark: "#F9FAFB" + fg-muted: + light: "#6B7280" + dark: "#9CA3AF" + fg-disabled: + light: "#D1D5DB" + dark: "#4B5563" + primary: + light: "#7C3AED" + dark: "#8B5CF6" + on-primary: + light: "#FFFFFF" + dark: "#FFFFFF" + primary-container: + light: "#EDE9FE" + dark: "#4C1D95" + accent: + light: "#4F46E5" + dark: "#6366F1" + border: + light: "#E5E7EB" + dark: "#374151" + border-subtle: + light: "#F3F4F6" + dark: "#1F2937" + destructive: + light: "#DC2626" + dark: "#EF4444" + on-destructive: + light: "#FFFFFF" + dark: "#FFFFFF" + destructive-container: + light: "#FEE2E2" + dark: "#7F1D1D" + success: + light: "#15803D" + dark: "#16A34A" + warning: + light: "#D97706" + dark: "#B45309" +typography: + display: + fontFamily: Inter + fontSize: 36px + fontWeight: "700" + lineHeight: 44px + letterSpacing: -0.02em + headline-lg: + fontFamily: Inter + fontSize: 28px + fontWeight: "700" + lineHeight: 36px + letterSpacing: -0.01em + headline-md: + fontFamily: Inter + fontSize: 22px + fontWeight: "600" + lineHeight: 30px + title-lg: + fontFamily: Inter + fontSize: 18px + fontWeight: "600" + lineHeight: 26px + body-lg: + fontFamily: Inter + fontSize: 16px + fontWeight: "400" + lineHeight: 26px + body-md: + fontFamily: Inter + fontSize: 14px + fontWeight: "400" + lineHeight: 22px + label-md: + fontFamily: Inter + fontSize: 14px + fontWeight: "600" + lineHeight: 20px + letterSpacing: 0.01em + label-sm: + fontFamily: Inter + fontSize: 12px + fontWeight: "500" + lineHeight: 16px +rounded: + sm: 4px + DEFAULT: 8px + md: 12px + lg: 16px + xl: 24px + full: 9999px +spacing: + xs: 4px + sm: 8px + md: 16px + lg: 24px + xl: 40px + 2xl: 64px + gutter: 16px + section: 32px +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.on-primary}" + rounded: "{rounded.md}" + padding: "{spacing.md}" + button-destructive: + backgroundColor: "{colors.destructive}" + textColor: "{colors.on-destructive}" + rounded: "{rounded.md}" + padding: "{spacing.md}" + card-surface: + backgroundColor: "{colors.surface-container}" + rounded: "{rounded.lg}" + padding: "{spacing.lg}" + card-elevated: + backgroundColor: "{colors.surface}" + rounded: "{rounded.lg}" + padding: "{spacing.lg}" + input-field: + backgroundColor: "{colors.surface-container-high}" + rounded: "{rounded.DEFAULT}" + padding: "{spacing.md}" + badge-primary: + backgroundColor: "{colors.primary-container}" + rounded: "{rounded.full}" + padding: "{spacing.xs}" +--- + +## Overview + +Magic Example is the reference consumer app for the magic framework plus +magic_starter starter kit. Its design system is built around a single violet +brand with Material 3 role semantics, Wind utility tokens, and a mobile-first +responsive layout. + +The brand personality is precise, professional, and approachable. The violet +primary anchors interactive surfaces (buttons, active tabs, focus rings) while +gray neutrals keep the reading experience calm. The accent indigo provides a +distinct secondary signal without introducing a second brand color. + +For the responsive direction and accessible usage patterns, see +[docs/design-culture/](docs/design-culture/). + +## Colors + +The palette uses the M3 role model mapped onto 17 Wind semantic alias keys. A +single consumer-supplied `primary` MaterialColor drives shade resolution across +the component system; nothing else is hardcoded. + +Light mode background hierarchy: `surface` (white page) -> `surface-container` +(cards) -> `surface-container-high` (input backgrounds). Dark mode inverts +toward near-black gray steps. + +Primary violet (#7C3AED light, #8B5CF6 dark) provides a 5.7:1 contrast ratio +against white, passing WCAG AA for normal text. Destructive red (#DC2626) and +on-destructive white also pass at 4.8:1. + +See [docs/design-culture/accessibility-wcag.md](docs/design-culture/accessibility-wcag.md) +for contrast requirements and how `design:lint` enforces them. + +## Typography + +Inter is the app font: geometric, legible on small mobile screens, and neutral +enough not to compete with the violet brand. All sizes are in logical pixels +aligned to a 4px grid. + +For type hierarchy guidance see +[docs/design-culture/refactoring-ui.md](docs/design-culture/refactoring-ui.md). + +## Layout + +The app uses a mobile-first 1-column layout that expands to a sidebar + content +column at the `md` breakpoint (768px). Spacing follows the 4px logical scale +defined in the `spacing` section above; `gutter` (16px) is the horizontal +content margin on narrow screens and `section` (32px) separates stacked +sections. + +For responsive layout patterns and breakpoint usage, see +[docs/design-culture/wind-responsive.md](docs/design-culture/wind-responsive.md). + +## Elevation & Depth + +Surface hierarchy is expressed through tonal background shifts, not drop +shadows. `surface-container` sits one level above `surface`; `surface-container-high` +is used for input backgrounds and nested panels. + +Subtle border lines (`border-color-border`) separate sections instead of +shadows, keeping the UI light and reducing visual noise. + +## Shapes + +Corner radii follow the 4px logical scale: + +- Inputs and small controls: `DEFAULT` (8px) for a modern, structured look. +- Cards and dialogs: `lg` (16px) to feel contained and distinct. +- Badges and chips: `full` (9999px) for a pill shape. +- Buttons: `md` (12px), balancing substance and friendliness. + +## Components + +Components are described in detail in +[docs/design-culture/material-design-3.md](docs/design-culture/material-design-3.md). + +Variant matrices for every component are available via `flutter run` -> +`/preview` (debug builds only). Run `dart run bin/dispatcher.dart design:lint` +to validate token usage; run `dart run bin/dispatcher.dart design:sync` to +regenerate the Wind theme from this file. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..b18d0e2 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.fluttersdk.magic_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.fluttersdk.magic_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b21998b --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/fluttersdk/magic_example/MainActivity.kt b/android/app/src/main/kotlin/com/fluttersdk/magic_example/MainActivity.kt new file mode 100644 index 0000000..1955909 --- /dev/null +++ b/android/app/src/main/kotlin/com/fluttersdk/magic_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.fluttersdk.magic_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/assets/lang/en.json b/assets/lang/en.json new file mode 100644 index 0000000..77fc176 --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,290 @@ +{ + "common": { + "welcome": "Welcome to ", + "loading": "Loading...", + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "delete": "Delete", + "edit": "Edit", + "go_to_dashboard": "Go to Dashboard", + "remove": "Remove", + "toggle_theme": "Toggle theme", + "unknown": "Unknown", + "upload": "Upload", + "user": "User", + "done": "Done", + "error_occurred": "An unexpected error occurred. Please try again.", + "page_of": ":current / :total" + }, + "validation": { + "required": "The :attribute field is required.", + "email": "The :attribute must be a valid email address.", + "min": "The :attribute must be at least :min characters.", + "max": "The :attribute may not be greater than :max characters.", + "confirmed": "The :attribute confirmation does not match.", + "accepted": "The :attribute must be accepted.", + "same": "The :attribute and :other must match." + }, + "app": { + "name": "My App" + }, + "attributes": { + "current_password": "Current Password", + "email": "Email Address", + "name": "Name", + "new_password": "New Password", + "password": "Password", + "password_confirmation": "Confirm Password", + "phone": "Phone Number", + "phone_country": "Country Code", + "role": "Role", + "timezone": "Timezone" + }, + "auth": { + "already_have_account": "Already have an account?", + "authentication_code": "Authentication Code", + "back_to_login": "Back to Sign In", + "challenge_failed": "Two-factor challenge failed. Please try again.", + "dont_have_account": "Don't have an account?", + "forgot_password": "Forgot password?", + "forgot_password_subtitle": "Enter your email to receive a reset link", + "forgot_password_title": "Forgot Password", + "invalid_response": "An unexpected error occurred. Please try again.", + "login_failed": "Login failed. Please check your credentials.", + "login_subtitle": "Sign in to your account", + "login_title": "Sign In", + "logout": "Logout", + "or_continue_with": "Or continue with", + "password_reset_failed": "Unable to reset password. Please try again.", + "password_reset_success": "Your password has been reset. You can now sign in.", + "profile": "Profile", + "recovery_code": "Recovery Code", + "register_failed": "Registration failed. Please try again.", + "register_subtitle": "Create your account to get started", + "register_title": "Create Account", + "remember_me": "Remember me", + "reset_link_failed": "Unable to send reset link. Please try again.", + "reset_link_sent": "A password reset link has been sent to your email.", + "reset_password_button": "Reset Password", + "reset_password_subtitle": "Enter your new password", + "reset_password_title": "Reset Password", + "send_reset_link": "Send Reset Link", + "sign_in": "Sign In", + "sign_in_with": "Sign in with :provider", + "sign_up": "Sign up", + "sign_up_with": "Sign up with :provider", + "signed_in_as": "Signed in as", + "two_factor_challenge": "Two-Factor Challenge", + "two_factor_code_description": "Please confirm access to your account by entering the authentication code from your authenticator application.", + "two_factor_recovery_description": "Please confirm access to your account by entering one of your emergency recovery codes.", + "use_authentication_code": "Use an authentication code", + "use_recovery_code": "Use a recovery code", + "verify": "Verify", + "agree_to_legal": "By creating an account, you agree to our", + "terms_of_service": "Terms of Service", + "privacy_policy": "Privacy Policy", + "legal_and": "and" + }, + "errors": { + "unexpected": "An unexpected error occurred. Please try again.", + "network_error": "Network connection failed. Please check your internet connection and try again.", + "network_timeout": "Request timed out. Please try again later." + }, + "fields": { + "email_placeholder": "you@example.com", + "name_placeholder": "Enter your full name", + "password_confirmation_placeholder": "Repeat your password", + "password_placeholder": "At least 8 characters", + "phone_country_placeholder": "Select country code", + "phone_placeholder": "+905301234567", + "otp_placeholder": "123456" + }, + "magic_starter": { + "auth": { + "continue_as_guest": "Continue as Guest", + "guest_login_error": "Unable to continue as guest. Please try again." + }, + "email_verification": { + "resend_button": "Resend Verification Email", + "section_title": "Email Verification", + "send_error": "Failed to send verification email. Please try again.", + "sent": "Verification email sent successfully.", + "unverified_description": "Your email address is not verified yet. Please verify it to secure your account.", + "unverified_title": "Email address not verified", + "verified": "Your email address is verified." + }, + "guest_upgrade": { + "button": "Upgrade Account", + "description": "Create a permanent account to keep your data and unlock all features.", + "title": "Upgrade Your Guest Account" + }, + "notifications": { + "fetch_error": "Failed to load notification preferences." + }, + "newsletter": { + "fetch_error": "Failed to load newsletter preferences.", + "section_description": "Receive product updates and announcements by email.", + "section_title": "Newsletter Preferences", + "subscribe_label": "Subscribe to product updates and news", + "subscribed_status": "You are currently subscribed.", + "toggle_button": "Save Preferences", + "toggle_label": "Receive Newsletter", + "unsubscribed_status": "You are not subscribed.", + "update_error": "Failed to update newsletter preferences." + }, + "otp": { + "back_button": "Back", + "code_label": "Verification Code", + "code_subtitle": "Enter the verification code sent to your phone number.", + "code_title": "Verify Phone Number", + "phone_subtitle": "Enter your phone number to receive a verification code.", + "phone_title": "Phone Verification", + "resend_link": "Resend code", + "send_code_button": "Send Code", + "send_error": "Failed to send verification code. Please try again.", + "verify_button": "Verify Code", + "verify_error": "Failed to verify code. Please try again." + }, + "profile": { + "delete_account": { + "button": "Delete Account", + "description": "Permanently delete your account and all associated data. This action cannot be undone.", + "guest_upgrade_description": "Convert your guest account to a full account to manage or delete your data.", + "guest_upgrade_title": "Account Upgrade Required", + "password_label": "Confirm your password", + "title": "Delete Account" + } + } + }, + "nav": { + "dashboard": "Dashboard", + "profile": "Profile", + "settings": "Settings", + "system": "System" + }, + "notifications": { + "empty": "No notifications", + "list_subtitle": "View and manage your notifications", + "load_failed": "Failed to load notifications", + "mark_all_read": "Mark all as read", + "preferences_description": "Manage how and when you receive notifications", + "preferences_title": "Notification Preferences", + "settings": "Notification Settings", + "title": "Notifications", + "view_all": "View all notifications", + "badge_overflow": "9+", + "no_preferences": "No notification preferences available.", + "channel_email": "Email", + "channel_in_app": "In-App", + "channel_push": "Push" + }, + "profile": { + "browser_sessions": "Browser Sessions", + "browser_sessions_description": "Manage and sign out your active sessions on other browsers and devices.", + "confirm_password": "Confirm Password", + "confirm_password_description": "Please confirm your password before continuing.", + "copy_recovery_codes": "Copy Recovery Codes", + "current_device": "Current device", + "delete_failed": "Failed to delete account. Please try again.", + "extended_information": "Extended Information", + "language_label": "Language", + "logout_other_sessions": "Log Out Other Browser Sessions", + "no_active_sessions": "No active sessions found.", + "other_sessions_revoke_error": "Failed to revoke other browser sessions.", + "password_update_failed": "Failed to update password. Please check your current password.", + "password_updated": "Password updated successfully.", + "phone_country_label": "Country Code", + "phone_label": "Phone Number", + "photo_delete_failed": "Failed to remove profile photo.", + "photo_deleted": "Profile photo removed.", + "photo_requirements": "JPG, GIF or PNG. Max size of 1MB.", + "photo_update_failed": "Failed to update profile photo.", + "photo_updated": "Profile photo updated successfully.", + "profile_information": "Profile Information", + "profile_photo": "Profile Photo", + "revoke": "Revoke", + "session_revoke_error": "Failed to revoke browser session.", + "sessions_fetch_error": "Failed to load browser sessions.", + "settings": "Profile Settings", + "settings_subtitle": "Manage your account information and preferences", + "timezone_label": "Timezone", + "timezone_search": "Search timezone", + "timezone_select": "Select timezone", + "two_factor_authentication": "Two-Factor Authentication", + "two_factor_code_label": "Authentication Code", + "two_factor_code_placeholder": "Enter the 6-digit code", + "two_factor_confirm": "Confirm", + "two_factor_confirm_failed": "Failed to confirm two-factor authentication.", + "two_factor_disable": "Disable", + "two_factor_disable_failed": "Failed to disable two-factor authentication.", + "two_factor_disabled_description": "Two-factor authentication is currently disabled for your account.", + "two_factor_enable": "Enable", + "two_factor_enable_failed": "Failed to enable two-factor authentication.", + "two_factor_enabled": "Two-factor authentication is enabled.", + "two_factor_enabled_description": "Your account is protected with two-factor authentication.", + "two_factor_manual_entry": "Manual entry key", + "two_factor_recovery_codes_description": "Store these recovery codes in a secure location. They can be used to access your account if you lose your authenticator device.", + "two_factor_recovery_codes_fetch_failed": "Failed to load recovery codes.", + "two_factor_recovery_codes_regenerate_failed": "Failed to regenerate recovery codes.", + "two_factor_regenerate_codes": "Regenerate Recovery Codes", + "two_factor_setup_description": "Scan the QR code using your authenticator app and enter the generated code to confirm setup.", + "two_factor_show_recovery_codes": "Show Recovery Codes", + "update_failed": "Failed to update profile. Please try again.", + "update_password": "Update Password", + "updated": "Profile updated successfully.", + "copy_recovery_codes_success": "Recovery codes copied to clipboard.", + "two_factor_auth": "Two-Factor Authentication", + "two_factor": { + "copy_codes": "Copy All Codes", + "invalid_code": "The provided two-factor authentication code was invalid." + } + }, + "teams": { + "accept_invitation": "Accept Invitation", + "accept_invitation_subtitle": "Join a team by accepting this invitation", + "accept_invite_failed": "Failed to accept invitation.", + "cancel_invite_failed": "Failed to cancel invitation.", + "cancel_invite_label": "Cancel Invitation", + "confirm_cancel_invite": "Are you sure you want to cancel this invitation?", + "confirm_remove_member": "Are you sure you want to remove this member from the team?", + "create_failed": "Failed to create team.", + "create_team": "Create New Team", + "create_team_subtitle": "Create a new team to collaborate with others", + "created": "Team created successfully.", + "current_members": "Current Members", + "feature_disabled": "Team features are not enabled for this application.", + "general_settings": "General", + "invite_accepted": "You have joined the team successfully.", + "invite_canceled": "Invitation canceled successfully.", + "invite_failed": "Failed to send invitation.", + "invite_member": "Invite Member", + "invite_sent": "Invitation sent successfully.", + "member_remove_failed": "Failed to remove member.", + "member_removed": "Member removed from team", + "no_invitations": "No pending invitations", + "no_members": "No team members yet", + "no_team_selected": "No team selected.", + "pending": "Pending", + "pending_invitations": "Pending Invitations", + "remove_member_label": "Remove Member", + "role_admin": "Admin", + "role_member": "Member", + "select_team": "Select a team", + "send_invite": "Send Invite", + "settings": "Team Settings", + "settings_subtitle": "Manage your team name, members, and invitations", + "switch_failed": "Failed to switch team.", + "team": "Team", + "team_name": "Team Name", + "update_failed": "Failed to update team.", + "updated": "Team updated successfully." + }, + "time": { + "days_ago": ":days d ago", + "hours_ago": ":hours h ago", + "just_now": "Just now", + "minutes_ago": ":minutes m ago", + "date_format": ":day/:month/:year" + } +} \ No newline at end of file diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..6df8428 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[{compose,docker-compose}.{yml,yaml}] +indent_size = 4 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..7be55e2 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,27 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.codex +/.cursor/ +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/backend/.npmrc b/backend/.npmrc new file mode 100644 index 0000000..495a6af --- /dev/null +++ b/backend/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +audit=true diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..5ad1377 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,58 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals. + +## Agentic Development + +Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow: + +```bash +composer require laravel/boost --dev + +php artisan boost:install +``` + +Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices. + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/backend/app/Actions/MagicStarter/AddTeamMember.php b/backend/app/Actions/MagicStarter/AddTeamMember.php new file mode 100644 index 0000000..22fdd3a --- /dev/null +++ b/backend/app/Actions/MagicStarter/AddTeamMember.php @@ -0,0 +1,28 @@ + $input The validated team data. + * @return Model The created team instance. + */ + public function create(Authenticatable $user, array $input): Model + { + // TODO: Implement team creation logic. + throw new RuntimeException('CreateTeam action not implemented. Publish and implement this stub.'); + } +} diff --git a/backend/app/Actions/MagicStarter/CreateUser.php b/backend/app/Actions/MagicStarter/CreateUser.php new file mode 100644 index 0000000..91672c3 --- /dev/null +++ b/backend/app/Actions/MagicStarter/CreateUser.php @@ -0,0 +1,25 @@ + $input The validated registration data. + * @return Authenticatable The created user instance. + */ + public function create(array $input): Authenticatable + { + // TODO: Implement user creation logic. + // Example: Create user model, hash password, create personal team. + throw new \RuntimeException('CreateUser action not implemented. Publish and implement this stub.'); + } +} diff --git a/backend/app/Actions/MagicStarter/DeleteTeam.php b/backend/app/Actions/MagicStarter/DeleteTeam.php new file mode 100644 index 0000000..a1b2805 --- /dev/null +++ b/backend/app/Actions/MagicStarter/DeleteTeam.php @@ -0,0 +1,24 @@ + $input The validated update data. + */ + public function update(Authenticatable $user, Model $team, array $input): void + { + // TODO: Implement team update logic. + // Example: authorize, update model attributes. + throw new \RuntimeException('UpdateTeam action not implemented. Publish and implement this stub.'); + } +} diff --git a/backend/app/Actions/MagicStarter/UpdateTeamMemberRole.php b/backend/app/Actions/MagicStarter/UpdateTeamMemberRole.php new file mode 100644 index 0000000..eed8611 --- /dev/null +++ b/backend/app/Actions/MagicStarter/UpdateTeamMemberRole.php @@ -0,0 +1,28 @@ + $input The validated password data. + */ + public function update(Authenticatable $user, array $input): void + { + // TODO: Implement password update logic. + // Example: check current_password, hash new password, save. + throw new \RuntimeException('UpdateUserPassword action not implemented. Publish and implement this stub.'); + } +} diff --git a/backend/app/Actions/MagicStarter/UpdateUserProfile.php b/backend/app/Actions/MagicStarter/UpdateUserProfile.php new file mode 100644 index 0000000..dc0a503 --- /dev/null +++ b/backend/app/Actions/MagicStarter/UpdateUserProfile.php @@ -0,0 +1,25 @@ + $input The validated profile data. + */ + public function update(Authenticatable $user, array $input): void + { + // TODO: Implement profile update logic. + // Example: handle unique email check, update name/email, save. + throw new \RuntimeException('UpdateUserProfile action not implemented. Publish and implement this stub.'); + } +} diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/backend/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + + */ + protected $fillable = [ + 'user_id', + 'name', + 'personal_team', + 'profile_photo_path', + ]; +} diff --git a/backend/app/Models/TeamInvitation.php b/backend/app/Models/TeamInvitation.php new file mode 100644 index 0000000..f83a8f4 --- /dev/null +++ b/backend/app/Models/TeamInvitation.php @@ -0,0 +1,11 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'phone', + 'phone_country', + 'locale', + 'timezone', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + 'two_factor_secret', + 'two_factor_recovery_codes', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'is_guest' => 'boolean', + ]; + } +} diff --git a/backend/app/Policies/TeamPolicy.php b/backend/app/Policies/TeamPolicy.php new file mode 100644 index 0000000..0e26508 --- /dev/null +++ b/backend/app/Policies/TeamPolicy.php @@ -0,0 +1,62 @@ +belongsToTeam($team); + } + + /** + * Determine whether the user can update the team. + */ + public function update(mixed $user, mixed $team): bool + { + return $user->ownsTeam($team); + } + + /** + * Determine whether the user can delete the team. + */ + public function delete(mixed $user, mixed $team): bool + { + return $user->ownsTeam($team); + } + + /** + * Determine whether the user can manage team members. + */ + public function manageMembers(mixed $user, mixed $team): bool + { + return $user->ownsTeam($team) || $user->hasTeamRole($team, 'admin'); + } + + /** + * Determine whether the user can manage team invitations. + */ + public function manageInvitations(mixed $user, mixed $team): bool + { + return $user->ownsTeam($team) || $user->hasTeamRole($team, 'admin'); + } + + /** + * Determine whether the user can switch to the team. + */ + public function switchTo(mixed $user, mixed $team): bool + { + return $user->belongsToTeam($team); + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/backend/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +handleCommand(new ArgvInput); + +exit($status); diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php new file mode 100644 index 0000000..7a2848f --- /dev/null +++ b/backend/bootstrap/app.php @@ -0,0 +1,21 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->shouldRenderJsonWhen( + fn (Request $request) => $request->is('api/*'), + ); + })->create(); diff --git a/backend/bootstrap/cache/.gitignore b/backend/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/backend/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.1.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/googleapis/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" + }, + "time": "2026-06-11T17:54:14+00:00" + }, + { + "name": "fluttersdk/magic-starter-laravel", + "version": "dev-master", + "dist": { + "type": "path", + "url": "../../magic-starter-laravel", + "reference": "c569489d9b86d81bca018d758dc21f46f52bc93a" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "illuminate/database": "^12.0|^13.0", + "illuminate/routing": "^12.0|^13.0", + "illuminate/support": "^12.0|^13.0", + "laravel/sanctum": "^4.0", + "laravel/socialite": "^5.0", + "onesignal/onesignal-php-api": "^5.3", + "php": "^8.2", + "pragmarx/google2fa": "^8.0|^9.0" + }, + "require-dev": { + "larastan/larastan": "^3.0", + "laravel/pint": "^1.27", + "orchestra/testbench": "^10.0", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "geoip2/geoip2": "Optional: for IP geolocation in session management" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "FlutterSdk\\MagicStarter\\MagicStarterServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "FlutterSdk\\MagicStarter\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "FlutterSdk\\MagicStarter\\Tests\\": "tests/", + "FlutterSdk\\MagicStarter\\Database\\Factories\\": "database/factories/" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpunit" + ], + "lint": [ + "vendor/bin/pint --test" + ], + "lint:fix": [ + "vendor/bin/pint" + ], + "analyse": [ + "vendor/bin/phpstan analyse --memory-limit=1G" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "FlutterSdk", + "email": "hello@fluttersdk.com" + } + ], + "description": "Magic Framework Laravel backend starter package.", + "homepage": "https://magic.fluttersdk.com/starter", + "keywords": [ + "auth", + "jetstream", + "laravel", + "profile", + "sanctum", + "starter", + "teams" + ], + "support": { + "docs": "https://magic.fluttersdk.com/packages/starter-laravel/getting-started/installation", + "issues": "https://github.com/fluttersdk/magic-starter-laravel/issues", + "source": "https://github.com/fluttersdk/magic-starter-laravel" + }, + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.12.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.3", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.5.1", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.12.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-06-23T15:29:02+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-06-02T12:23:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.12.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.12.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-06-23T15:21:08+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-06-23T13:02:23+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.17.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "0802b7a81f3252d78200b8037ac183a686a529f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/0802b7a81f3252d78200b8037ac183a686a529f0", + "reference": "0802b7a81f3252d78200b8037ac183a686a529f0", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/psr7": "^2.9", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "^2.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-06-23T19:42:45+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.19", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "ac8ac2c3ec598df031dcd474a002a51b7b6e69a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/ac8ac2c3ec598df031dcd474a002a51b7b6e69a5", + "reference": "ac8ac2c3ec598df031dcd474a002a51b7b6e69a5", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.19" + }, + "time": "2026-06-23T01:06:19+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-04-30T11:46:25+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/socialite", + "version": "v5.28.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "4c131ff4b24d8881a9c8fe4eecb5ffeff9803f26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/4c131ff4b24d8881a9c8fe4eecb5ffeff9803f26", + "reference": "4c131ff4b24d8881a9c8fe4eecb5ffeff9803f26", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4|^7.0", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "league/oauth1-client": "^1.11", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.12.23", + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2026-06-12T03:24:05+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.35.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "d0a405f03d980461e4b6e08cfed2c162650f9f6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d0a405f03d980461e4b6e08cfed2c162650f9f6b", + "reference": "d0a405f03d980461e4b6e08cfed2c162650f9f6b", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.35.0" + }, + "time": "2026-06-22T20:12:06+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/oauth1-client", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.13.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-06-18T13:49:15+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "onesignal/onesignal-php-api", + "version": "5.8.0", + "source": { + "type": "git", + "url": "https://github.com/OneSignal/onesignal-php-api.git", + "reference": "4da2d2aea68c1df8b460ad482fcfddbfd79549c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OneSignal/onesignal-php-api/zipball/4da2d2aea68c1df8b460ad482fcfddbfd79549c9", + "reference": "4da2d2aea68c1df8b460ad482fcfddbfd79549c9", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.3", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "onesignal\\client\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "authors": [ + { + "name": "OneSignal developers", + "homepage": "https://onesignal.com" + } + ], + "description": "A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com", + "homepage": "https://onesignal.com", + "keywords": [ + "api", + "onesignal", + "php", + "rest", + "sdk" + ], + "support": { + "issues": "https://github.com/OneSignal/onesignal-php-api/issues", + "source": "https://github.com/OneSignal/onesignal-php-api/tree/v5.8.0" + }, + "time": "2026-06-24T04:17:13+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.55", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "db9744e6d47e742b1f974e965ad49bdd041105af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db9744e6d47e742b1f974e965ad49bdd041105af", + "reference": "db9744e6d47e742b1f974e965ad49bdd041105af", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.55" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-06-14T23:24:10+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + }, + "time": "2025-09-19T22:51:08+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.23", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" + }, + "time": "2026-05-23T13:41:31+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", + "shasum": "" + }, + "require": { + "brick/math": ">=0.8.16 <=0.18", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.3" + }, + "time": "2026-06-18T03:57:49+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "58d2e767a66052c1487356f953445634a8194c64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", + "reference": "58d2e767a66052c1487356f953445634a8194c64", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/af11474600f06718086c2cda4fa6fa8d0a672e7e", + "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", + "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T08:46:08+00:00" + }, + { + "name": "symfony/mailer", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "9418d772df3a03a142e3bc06f602adb2b8724877" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/9418d772df3a03a142e3bc06f602adb2b8724877", + "reference": "9418d772df3a03a142e3bc06f602adb2b8724877", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.4.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/mime", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664", + "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "dc21118016c039a66235cf93d96b435ffb282412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T15:22:23+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T02:25:22+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T11:52:35+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/uid", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7393f157a55f7e70a4de0334435c55a5a8fe749a", + "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e", + "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/agent-detector", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/agent-detector.git", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/agent-detector/zipball/90694b9256099591cf9e55d08c18ba7a00bf099f", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f", + "shasum": "" + }, + "require": { + "php": "^8.2.0" + }, + "require-dev": { + "laravel/pint": "^1.24.0", + "pestphp/pest": "^3.8.5|^4.1.0", + "pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2", + "phpstan/phpstan": "^2.1.26", + "rector/rector": "^2.1.7", + "symfony/var-dumper": "^7.3.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Laravel\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Detect if code is running in an AI agent or automated development environment", + "homepage": "https://github.com/laravel/agent-detector", + "keywords": [ + "Agent", + "ai", + "automation", + "claude", + "cursor", + "detection", + "devin", + "php" + ], + "support": { + "issues": "https://github.com/laravel/agent-detector/issues", + "source": "https://github.com/laravel/agent-detector" + }, + "time": "2026-04-29T18:32:34+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/pao", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/pao.git", + "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pao/zipball/41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "shasum": "" + }, + "require": { + "laravel/agent-detector": "^2.0.2", + "php": "^8.3" + }, + "conflict": { + "laravel/framework": "<12.0.0", + "nunomaduro/collision": "<8.9.3", + "pestphp/pest": "<4.6.3 || >=6.0.0", + "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.20.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench": "^10.11.0 || ^11.1.0", + "pestphp/pest": "^4.7.2 || ^5.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0", + "phpstan/phpstan": "^2.2.2", + "rector/rector": "^2.4.5", + "symfony/process": "^7.4.8 || ^8.1.0", + "symfony/var-dumper": "^7.4.8 || ^8.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Laravel\\Pao\\Drivers\\Pest\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Laravel\\Pao\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Laravel\\Pao\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Agent-optimized output for PHP testing tools", + "keywords": [ + "Agent", + "PHPStan", + "ai", + "dev", + "paratest", + "pest", + "php", + "phpunit", + "rector", + "testing" + ], + "support": { + "issues": "https://github.com/laravel/pao/issues", + "source": "https://github.com/laravel/pao" + }, + "time": "2026-06-22T19:58:00+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", + "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-06-16T15:34:04+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "186dab580576598076de6818596d12b61801880e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.28" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-06-01T13:24:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.30", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.30" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-06-15T13:12:30+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "fluttersdk/magic-starter-laravel": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/backend/config/app.php b/backend/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/backend/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/backend/config/auth.php b/backend/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/backend/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/backend/config/cache.php b/backend/config/cache.php new file mode 100644 index 0000000..d7eec61 --- /dev/null +++ b/backend/config/cache.php @@ -0,0 +1,136 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "storage", "octane", + | "session", "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'storage' => [ + 'driver' => 'storage', + 'disk' => env('CACHE_STORAGE_DISK'), + 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, + +]; diff --git a/backend/config/database.php b/backend/config/database.php new file mode 100644 index 0000000..abbb88e --- /dev/null +++ b/backend/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/backend/config/filesystems.php b/backend/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/backend/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/backend/config/logging.php b/backend/config/logging.php new file mode 100644 index 0000000..b09cb25 --- /dev/null +++ b/backend/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/backend/config/magic-starter.php b/backend/config/magic-starter.php new file mode 100644 index 0000000..c927a97 --- /dev/null +++ b/backend/config/magic-starter.php @@ -0,0 +1,298 @@ + false, + + /* + |-------------------------------------------------------------------------- + | Feature Flags + |-------------------------------------------------------------------------- + | + | Enable or disable package features. Each feature is a string constant + | defined on the Features class. Disabled features are simply omitted + | from this array. + | + */ + + 'features' => [ + \FlutterSdk\MagicStarter\Features::twoFactorAuthentication(), + \FlutterSdk\MagicStarter\Features::teams(), + // \FlutterSdk\MagicStarter\Features::profilePhotos(), + \FlutterSdk\MagicStarter\Features::sessions(), + // \FlutterSdk\MagicStarter\Features::socialLogin(), + // \FlutterSdk\MagicStarter\Features::newsletterSubscription(), + // \FlutterSdk\MagicStarter\Features::extendedProfile(), + // \FlutterSdk\MagicStarter\Features::notifications(), + // \FlutterSdk\MagicStarter\Features::onesignal(), + // \FlutterSdk\MagicStarter\Features::guestAuth(), + // \FlutterSdk\MagicStarter\Features::phoneOtp(), + \FlutterSdk\MagicStarter\Features::emailVerification(), + // \FlutterSdk\MagicStarter\Features::timezones(), + ], + + /* + |-------------------------------------------------------------------------- + | Frontend URL + |-------------------------------------------------------------------------- + | + | The URL of the frontend application that will consume the API provided by + | this package. This is used when sending email invitations to teams, so + | that the links in the email point to the correct frontend application. + | + */ + + 'frontend_url' => env('MAGIC_STARTER_FRONTEND_URL'), + + /* + |-------------------------------------------------------------------------- + | Models + |-------------------------------------------------------------------------- + | + | Override the default Eloquent models used by the package. + | + */ + + 'models' => [ + 'user' => env('MAGIC_STARTER_USER_MODEL'), + 'team' => env('MAGIC_STARTER_TEAM_MODEL', Team::class), + 'membership' => env('MAGIC_STARTER_MEMBERSHIP_MODEL', TeamUser::class), + 'team_invitation' => env('MAGIC_STARTER_TEAM_INVITATION_MODEL', TeamInvitation::class), + ], + + /* + |-------------------------------------------------------------------------- + | Locale & Timezone + |-------------------------------------------------------------------------- + | + | Default locale and timezone for new users. These values are used when + | creating a new user account and can be updated by the user later. + | + | When the client sends Accept-Language or X-Timezone headers during + | registration, the package auto-detects values from those headers and + | validates them against the supported lists below. + | + */ + + 'defaults' => [ + 'locale' => env('MAGIC_STARTER_DEFAULT_LOCALE', 'en'), + 'timezone' => env('MAGIC_STARTER_DEFAULT_TIMEZONE', 'UTC'), + ], + + /* + |-------------------------------------------------------------------------- + | Supported Locales + |-------------------------------------------------------------------------- + | + | The list of locale codes your application supports. Used for validation + | during registration and profile updates, and for auto-detection from + | the Accept-Language header. Locale codes should be 2-letter ISO 639-1. + | + */ + + 'supported_locales' => [ + 'en', + 'tr', + ], + + /* + |-------------------------------------------------------------------------- + | Profile & Team Photos + |-------------------------------------------------------------------------- + | + | Configure the storage disk, paths, and fallback Avatar generator URL + | for user profile photos and team profile photos. + | + */ + + 'profile_photo_disk' => env('MAGIC_STARTER_PROFILE_PHOTO_DISK', 'public'), + 'team_photo_disk' => env('MAGIC_STARTER_TEAM_PHOTO_DISK', env('MAGIC_STARTER_PROFILE_PHOTO_DISK', 'public')), + 'profile_photo_path' => env('MAGIC_STARTER_PROFILE_PHOTO_PATH', 'profile-photos'), + 'team_photo_path' => env('MAGIC_STARTER_TEAM_PHOTO_PATH', 'team-photos'), + 'ui_avatars_url' => env('MAGIC_STARTER_UI_AVATARS_URL', 'https://ui-avatars.com/api/'), + + /* + |-------------------------------------------------------------------------- + | Route Prefix + |-------------------------------------------------------------------------- + | + | Prefix for all routes registered by this package. + | + */ + + 'route_prefix' => env('MAGIC_STARTER_ROUTE_PREFIX', 'api/v1'), + + /* + |-------------------------------------------------------------------------- + | Team Invitation Expiry + |-------------------------------------------------------------------------- + | + | Determines the number of days until a team invitation expires. + | + */ + + 'invitation_expiry_days' => env('MAGIC_STARTER_INVITATION_EXPIRY_DAYS', 7), + + /* + |-------------------------------------------------------------------------- + | Token Expiration + |-------------------------------------------------------------------------- + | + | Set the number of minutes until issued tokens expire. Null means + | tokens never expire. Configure Sanctum's pruning command to clean + | up expired tokens: php artisan sanctum:prune-expired --hours=24 + | + */ + + 'token_expiration_minutes' => env('MAGIC_STARTER_TOKEN_EXPIRATION', null), + + /* + |-------------------------------------------------------------------------- + | Authentication Identity + |-------------------------------------------------------------------------- + | + | Configure which identity fields are accepted during registration and + | login. Both can be enabled simultaneously — in that case, at least one + | identifier is required. + | + | - email: true → users may register/login with an email address + | - phone: true → users may register/login with a phone number + | + | When both are true, the register and login forms accept either or both. + | When only one is true, that identifier becomes required. + | + */ + + 'auth' => [ + 'email' => (bool) env('MAGIC_STARTER_AUTH_EMAIL', true), + 'phone' => (bool) env('MAGIC_STARTER_AUTH_PHONE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Two-Factor Authentication + |-------------------------------------------------------------------------- + | + | Configure the settings for Two-Factor Authentication (2FA). This includes + | the company name displayed in authenticator apps, the number of recovery + | codes to generate, and the TTL for the challenge token. + | + */ + + 'two_factor' => [ + /* + |-------------------------------------------------------------------------- + | Company Name + |-------------------------------------------------------------------------- + | + | The name of your company or application as it will appear in the + | user's authenticator app (e.g., Google Authenticator, Authy). + | + */ + + 'company_name' => env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Recovery Codes Count + |-------------------------------------------------------------------------- + | + | The number of multi-use recovery codes that should be generated for + | the user when they enable two-factor authentication. + | + */ + + 'recovery_codes_count' => 8, + + /* + |-------------------------------------------------------------------------- + | GeoIP Database Path + |-------------------------------------------------------------------------- + | + | The absolute path to the MaxMind GeoIP2 database file (.mmdb) used to + | resolve location data for 2FA challenge attempts. Set to null to + | disable location resolution. + | + */ + + 'geoip_db_path' => null, + + /* + |-------------------------------------------------------------------------- + | Challenge Token TTL + |-------------------------------------------------------------------------- + | + | The number of minutes a two-factor authentication challenge token is + | valid for. Users must complete the challenge within this window. + | + */ + + 'challenge_token_ttl' => 5, + ], + + /* + |-------------------------------------------------------------------------- + | OneSignal Push Notifications + |-------------------------------------------------------------------------- + | + | Configure the settings for OneSignal push notifications. This includes + | the app ID, REST API key, and the default target channel for push messages. + | + */ + + 'onesignal' => [ + /* + |-------------------------------------------------------------------------- + | OneSignal App ID + |-------------------------------------------------------------------------- + | + | The OneSignal application ID for your project. Required to send push + | notifications via the OneSignal PHP SDK. + | + */ + + 'app_id' => env('ONESIGNAL_APP_ID'), + + /* + |-------------------------------------------------------------------------- + | OneSignal REST API Key + |-------------------------------------------------------------------------- + | + | The OneSignal REST API key for server-to-server authentication. + | Required to send push notifications via the OneSignal PHP SDK. + | + */ + + 'rest_api_key' => env('ONESIGNAL_REST_API_KEY'), + + /* + |-------------------------------------------------------------------------- + | OneSignal Target Channel + |-------------------------------------------------------------------------- + | + | The default delivery channel for OneSignal push notifications. + | Typically 'push' for native mobile push notifications. + | + */ + + 'target_channel' => 'push', + ], +]; diff --git a/backend/config/mail.php b/backend/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/backend/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/backend/config/queue.php b/backend/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/backend/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/backend/config/services.php b/backend/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/backend/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/backend/config/session.php b/backend/config/session.php new file mode 100644 index 0000000..f574482 --- /dev/null +++ b/backend/config/session.php @@ -0,0 +1,233 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | Session Serialization + |-------------------------------------------------------------------------- + | + | This value controls the serialization strategy for session data, which + | is JSON by default. Setting this to "php" allows the storage of PHP + | objects in the session but can make an application vulnerable to + | "gadget chain" serialization attacks if the APP_KEY is leaked. + | + | Supported: "json", "php" + | + */ + + 'serialization' => 'json', + +]; diff --git a/backend/database/.gitignore b/backend/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/backend/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php new file mode 100644 index 0000000..c4ceb07 --- /dev/null +++ b/backend/database/factories/UserFactory.php @@ -0,0 +1,45 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000001_create_cache_table.php b/backend/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..06dc7a5 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->bigInteger('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->bigInteger('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000002_create_jobs_table.php b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..edac6fe --- /dev/null +++ b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,59 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedSmallInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->string('connection'); + $table->string('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100010_create_teams_table.php b/backend/database/migrations/2026_06_24_100010_create_teams_table.php new file mode 100644 index 0000000..380384e --- /dev/null +++ b/backend/database/migrations/2026_06_24_100010_create_teams_table.php @@ -0,0 +1,28 @@ +constrained()->cascadeOnDelete(); + $table->string('name'); + $table->boolean('personal_team')->default(true); + $table->string('profile_photo_path', 2048)->nullable(); + $table->timestamps(); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('teams'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100020_create_team_user_table.php b/backend/database/migrations/2026_06_24_100020_create_team_user_table.php new file mode 100644 index 0000000..23c6734 --- /dev/null +++ b/backend/database/migrations/2026_06_24_100020_create_team_user_table.php @@ -0,0 +1,29 @@ +constrained()->cascadeOnDelete(); + MigrationHelper::foreignKey($table, 'user_id')->constrained()->cascadeOnDelete(); + $table->string('role')->nullable()->default('member'); + $table->timestamps(); + + $table->unique(['team_id', 'user_id']); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('team_user'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100030_create_team_invitations_table.php b/backend/database/migrations/2026_06_24_100030_create_team_invitations_table.php new file mode 100644 index 0000000..5265ffe --- /dev/null +++ b/backend/database/migrations/2026_06_24_100030_create_team_invitations_table.php @@ -0,0 +1,30 @@ +constrained()->cascadeOnDelete(); + $table->string('email'); + $table->string('role')->nullable()->default('member'); + $table->string('token')->unique(); + $table->timestamps(); + + $table->unique(['team_id', 'email']); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('team_invitations'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100040_create_personal_access_tokens_table.php b/backend/database/migrations/2026_06_24_100040_create_personal_access_tokens_table.php new file mode 100644 index 0000000..585ea13 --- /dev/null +++ b/backend/database/migrations/2026_06_24_100040_create_personal_access_tokens_table.php @@ -0,0 +1,32 @@ +text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->timestamps(); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100050_create_notifications_table.php b/backend/database/migrations/2026_06_24_100050_create_notifications_table.php new file mode 100644 index 0000000..9e0a0f2 --- /dev/null +++ b/backend/database/migrations/2026_06_24_100050_create_notifications_table.php @@ -0,0 +1,40 @@ +string('type'); + MigrationHelper::morphColumns($table, 'notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + + $table->index([ + 'notifiable_type', + 'notifiable_id', + 'read_at', + ]); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100060_create_notification_settings_table.php b/backend/database/migrations/2026_06_24_100060_create_notification_settings_table.php new file mode 100644 index 0000000..314ea4e --- /dev/null +++ b/backend/database/migrations/2026_06_24_100060_create_notification_settings_table.php @@ -0,0 +1,41 @@ +string('type'); + $table->string('channel'); + $table->boolean('is_enabled')->default(true); + $table->timestamps(); + + $table->unique([ + 'notifiable_id', + 'notifiable_type', + 'type', + 'channel', + ], 'notification_settings_unique'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notification_settings'); + } +}; diff --git a/backend/database/migrations/2026_06_24_100070_create_newsletter_subscribers_table.php b/backend/database/migrations/2026_06_24_100070_create_newsletter_subscribers_table.php new file mode 100644 index 0000000..65323aa --- /dev/null +++ b/backend/database/migrations/2026_06_24_100070_create_newsletter_subscribers_table.php @@ -0,0 +1,27 @@ +string('email')->unique(); + $table->boolean('is_active')->default(true); + $table->string('source')->nullable(); + $table->timestamps(); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('newsletter_subscribers'); + } +}; diff --git a/backend/database/migrations/2026_06_24_200010_add_current_team_id_to_users_table.php b/backend/database/migrations/2026_06_24_200010_add_current_team_id_to_users_table.php new file mode 100644 index 0000000..30a50cf --- /dev/null +++ b/backend/database/migrations/2026_06_24_200010_add_current_team_id_to_users_table.php @@ -0,0 +1,28 @@ +nullable() + ->constrained('teams') + ->nullOnDelete(); + }); + } + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropConstrainedForeignId('current_team_id'); + }); + } +}; diff --git a/backend/database/migrations/2026_06_24_200020_add_device_info_to_personal_access_tokens_table.php b/backend/database/migrations/2026_06_24_200020_add_device_info_to_personal_access_tokens_table.php new file mode 100644 index 0000000..1d1ccd9 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200020_add_device_info_to_personal_access_tokens_table.php @@ -0,0 +1,35 @@ +string('ip_address', 45)->nullable(); + } + + if (! Schema::hasColumn('personal_access_tokens', 'user_agent')) { + $table->text('user_agent')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('personal_access_tokens', function (Blueprint $table) { + $columnsToDrop = array_filter([ + Schema::hasColumn('personal_access_tokens', 'ip_address') ? 'ip_address' : null, + Schema::hasColumn('personal_access_tokens', 'user_agent') ? 'user_agent' : null, + ]); + + if (count($columnsToDrop) > 0) { + $table->dropColumn($columnsToDrop); + } + }); + } +}; diff --git a/backend/database/migrations/2026_06_24_200030_add_expires_at_to_team_invitations_table.php b/backend/database/migrations/2026_06_24_200030_add_expires_at_to_team_invitations_table.php new file mode 100644 index 0000000..bcf0265 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200030_add_expires_at_to_team_invitations_table.php @@ -0,0 +1,26 @@ +timestamp('expires_at')->nullable()->after('token'); + }); + } + } + + public function down(): void + { + if (Schema::hasColumn('team_invitations', 'expires_at')) { + Schema::table('team_invitations', function (Blueprint $table): void { + $table->dropColumn('expires_at'); + }); + } + } +}; diff --git a/backend/database/migrations/2026_06_24_200040_add_guest_and_phone_fields_to_users_table.php b/backend/database/migrations/2026_06_24_200040_add_guest_and_phone_fields_to_users_table.php new file mode 100644 index 0000000..f0ce8c6 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200040_add_guest_and_phone_fields_to_users_table.php @@ -0,0 +1,66 @@ +string('email')->nullable()->change(); + $table->string('password')->nullable()->change(); + + if (! Schema::hasColumn('users', 'is_guest')) { + $table->boolean('is_guest') + ->default(false) + ->after('password'); + } + + if (! Schema::hasColumn('users', 'device_id')) { + $table->string('device_id', 255) + ->nullable() + ->unique() + ->after('is_guest'); + } + + if (! Schema::hasColumn('users', 'phone_country')) { + $table->char('phone_country', 2) + ->nullable() + ->after('phone'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('email')->nullable(false)->change(); + $table->string('password')->nullable(false)->change(); + + if (Config::get('database.default') !== 'sqlite') { + if (Schema::hasColumn('users', 'device_id')) { + $table->dropUnique(['device_id']); + } + } + + $columnsToDrop = array_filter([ + Schema::hasColumn('users', 'is_guest') ? 'is_guest' : null, + Schema::hasColumn('users', 'device_id') ? 'device_id' : null, + Schema::hasColumn('users', 'phone_country') ? 'phone_country' : null, + ]); + + if (count($columnsToDrop) > 0) { + $table->dropColumn($columnsToDrop); + } + }); + } +}; diff --git a/backend/database/migrations/2026_06_24_200050_add_localization_fields_to_users_table.php b/backend/database/migrations/2026_06_24_200050_add_localization_fields_to_users_table.php new file mode 100644 index 0000000..554b129 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200050_add_localization_fields_to_users_table.php @@ -0,0 +1,35 @@ +string('locale')->default('en')->after('remember_token'); + } + + if (! Schema::hasColumn('users', 'timezone')) { + $table->string('timezone')->default('UTC')->after('locale'); + } + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $columnsToDrop = array_filter([ + Schema::hasColumn('users', 'locale') ? 'locale' : null, + Schema::hasColumn('users', 'timezone') ? 'timezone' : null, + ]); + + if (count($columnsToDrop) > 0) { + $table->dropColumn($columnsToDrop); + } + }); + } +}; diff --git a/backend/database/migrations/2026_06_24_200060_add_profile_fields_to_users_table.php b/backend/database/migrations/2026_06_24_200060_add_profile_fields_to_users_table.php new file mode 100644 index 0000000..a3397e4 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200060_add_profile_fields_to_users_table.php @@ -0,0 +1,26 @@ +string('phone')->nullable()->after('email'); + } + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (Schema::hasColumn('users', 'phone')) { + $table->dropColumn('phone'); + } + }); + } +}; diff --git a/backend/database/migrations/2026_06_24_200070_add_profile_photo_path_to_teams_table.php b/backend/database/migrations/2026_06_24_200070_add_profile_photo_path_to_teams_table.php new file mode 100644 index 0000000..51dec45 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200070_add_profile_photo_path_to_teams_table.php @@ -0,0 +1,26 @@ +string('profile_photo_path', 2048)->nullable()->after('personal_team'); + }); + } + } + + public function down(): void + { + if (Schema::hasColumn('teams', 'profile_photo_path')) { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('profile_photo_path'); + }); + } + } +}; diff --git a/backend/database/migrations/2026_06_24_200080_add_profile_photo_path_to_users_table.php b/backend/database/migrations/2026_06_24_200080_add_profile_photo_path_to_users_table.php new file mode 100644 index 0000000..209d6bb --- /dev/null +++ b/backend/database/migrations/2026_06_24_200080_add_profile_photo_path_to_users_table.php @@ -0,0 +1,26 @@ +string('profile_photo_path', 2048)->nullable()->after('email'); + }); + } + } + + public function down(): void + { + if (Schema::hasColumn('users', 'profile_photo_path')) { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('profile_photo_path'); + }); + } + } +}; diff --git a/backend/database/migrations/2026_06_24_200090_add_timezone_to_users_table.php b/backend/database/migrations/2026_06_24_200090_add_timezone_to_users_table.php new file mode 100644 index 0000000..258dc32 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200090_add_timezone_to_users_table.php @@ -0,0 +1,26 @@ +string('timezone')->default('UTC')->after('remember_token'); + } + }); + } + + public function down(): void + { + if (Schema::hasColumn('users', 'timezone')) { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('timezone'); + }); + } + } +}; diff --git a/backend/database/migrations/2026_06_24_200100_add_two_factor_columns_to_users_table.php b/backend/database/migrations/2026_06_24_200100_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..38ccd34 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200100_add_two_factor_columns_to_users_table.php @@ -0,0 +1,67 @@ +getTable(); + + Schema::table($tableName, function (Blueprint $table) use ($tableName) { + if (! Schema::hasColumn($tableName, 'two_factor_secret')) { + $table->text('two_factor_secret') + ->after('password') + ->nullable(); + } + + if (! Schema::hasColumn($tableName, 'two_factor_recovery_codes')) { + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + } + + if (! Schema::hasColumn($tableName, 'two_factor_confirmed_at')) { + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $userModel = MagicStarter::userModel(); + $tableName = (new $userModel)->getTable(); + + Schema::table($tableName, function (Blueprint $table) use ($tableName) { + $columnsToDrop = []; + + if (Schema::hasColumn($tableName, 'two_factor_secret')) { + $columnsToDrop[] = 'two_factor_secret'; + } + + if (Schema::hasColumn($tableName, 'two_factor_recovery_codes')) { + $columnsToDrop[] = 'two_factor_recovery_codes'; + } + + if (Schema::hasColumn($tableName, 'two_factor_confirmed_at')) { + $columnsToDrop[] = 'two_factor_confirmed_at'; + } + + if (count($columnsToDrop) > 0) { + $table->dropColumn($columnsToDrop); + } + }); + } +}; diff --git a/backend/database/migrations/2026_06_24_200110_drop_language_column_from_users_table.php b/backend/database/migrations/2026_06_24_200110_drop_language_column_from_users_table.php new file mode 100644 index 0000000..ad37539 --- /dev/null +++ b/backend/database/migrations/2026_06_24_200110_drop_language_column_from_users_table.php @@ -0,0 +1,26 @@ +dropColumn('language'); + } + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (! Schema::hasColumn('users', 'language')) { + $table->string('language', 10)->nullable()->after('timezone'); + } + }); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..6b901f8 --- /dev/null +++ b/backend/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/backend/lang/vendor/magic-starter/en/teams.php b/backend/lang/vendor/magic-starter/en/teams.php new file mode 100644 index 0000000..0b01f94 --- /dev/null +++ b/backend/lang/vendor/magic-starter/en/teams.php @@ -0,0 +1,6 @@ + ":name's Team", + 'guest_name' => 'Guest', +]; diff --git a/backend/lang/vendor/magic-starter/tr/teams.php b/backend/lang/vendor/magic-starter/tr/teams.php new file mode 100644 index 0000000..8c98ab7 --- /dev/null +++ b/backend/lang/vendor/magic-starter/tr/teams.php @@ -0,0 +1,6 @@ + ':name Takımı', + 'guest_name' => 'Misafir', +]; diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..49c869e --- /dev/null +++ b/backend/package.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^3.1", + "tailwindcss": "^4.0.0", + "vite": "^8.0.0" + } +} diff --git a/backend/phpunit.xml b/backend/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/backend/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/backend/public/.htaccess b/backend/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/backend/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/backend/public/favicon.ico b/backend/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/backend/public/index.php b/backend/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/backend/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/backend/public/robots.txt b/backend/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/backend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/backend/resources/css/app.css b/backend/resources/css/app.css new file mode 100644 index 0000000..54b247e --- /dev/null +++ b/backend/resources/css/app.css @@ -0,0 +1,9 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/backend/resources/js/app.js b/backend/resources/js/app.js new file mode 100644 index 0000000..8337712 --- /dev/null +++ b/backend/resources/js/app.js @@ -0,0 +1 @@ +// diff --git a/backend/resources/views/welcome.blade.php b/backend/resources/views/welcome.blade.php new file mode 100644 index 0000000..26e294a --- /dev/null +++ b/backend/resources/views/welcome.blade.php @@ -0,0 +1,223 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + @fonts + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

With so many options available to you,
we suggest you start with the following:

+ + + +

+ v{{ app()->version() }} + + View changelog + + + + +

+
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- 13 --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/backend/routes/console.php b/backend/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/backend/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/backend/routes/web.php b/backend/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/backend/routes/web.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/backend/tests/TestCase.php b/backend/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/backend/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/backend/vite.config.js b/backend/vite.config.js new file mode 100644 index 0000000..1fd66d5 --- /dev/null +++ b/backend/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import { bunny } from 'laravel-vite-plugin/fonts'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + fonts: [ + bunny('Instrument Sans', { + weights: [400, 500, 600], + }), + ], + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +}); diff --git a/bin/dispatcher.dart b/bin/dispatcher.dart new file mode 100644 index 0000000..67208b1 --- /dev/null +++ b/bin/dispatcher.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:fluttersdk_artisan/artisan.dart'; +import 'package:magic_example/app/commands/_index.g.dart' as consumer_commands; +import 'package:magic_example/app/_plugins.g.dart' as plugins; + +/// GENERATED by the `install` command. DO NOT EDIT. +/// +/// Wires: +/// +/// - consumer commands from `lib/app/commands/` via the codegen barrel +/// `lib/app/commands/_index.g.dart` (regenerated by every +/// `make:command` invocation in the consumer). +/// - plugin providers via `lib/app/_plugins.g.dart` (regenerated by +/// `plugin:install ` / `plugins:refresh` from the +/// `.artisan/plugins.json` registry). +/// +/// New plugins surface automatically after `plugin:install ` AND +/// the parent app's pubspec.yaml lists the plugin under `dependencies:`. +/// No manual edit to this file is required. +class _ConsumerCommandsProvider extends ArtisanServiceProvider { + @override + List commands() => consumer_commands.commands; +} + +Future main(List args) async { + exit( + await runArtisan( + args, + baseProviders: [ + _ConsumerCommandsProvider(), + ...plugins.autoDiscoveredProviders(), + ], + delegateToConsumer: false, + collectMcpTools: args.isNotEmpty && args.first == 'mcp:serve', + ), + ); +} diff --git a/bin/fsa b/bin/fsa new file mode 100755 index 0000000..8a853d0 --- /dev/null +++ b/bin/fsa @@ -0,0 +1,106 @@ +#!/usr/bin/env sh +# bin/fsa: generated by 'dart run fluttersdk_artisan make:fast-cli'. +# Rebuilds the artisan CLI native binary when pubspec.lock or Dart SDK changes. +# POSIX sh; tested on macOS + Linux. +# Re-run 'make:fast-cli --force' to regenerate this wrapper. + +set -e + +# Resolve real path through symlinks without readlink -f (macOS compatible). +follow_links() ( + cd -P -- "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [ -h "$file" ]; do + cd -P -- "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P -- "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" +) + +# 1. Resolve locations relative to this script. +PROG_NAME="$(follow_links "$0")" +BIN_DIR="$(cd -P -- "$(dirname -- "$PROG_NAME")" && pwd)" +PROJECT_ROOT="$(cd -P -- "$BIN_DIR/.." && pwd)" + +# 2. Define cache paths. +CACHE_DIR="$PROJECT_ROOT/.artisan/cli-bundle" +STAMP_FILE="$PROJECT_ROOT/.artisan/build.stamp" +BINARY="$CACHE_DIR/bundle/bin/dispatcher" +LOCK_DIR="$PROJECT_ROOT/.artisan/.fsa.lock" +ENTRY="$PROJECT_ROOT/bin/dispatcher.dart" + +# 3. Compute a compile key from pubspec.lock hash and Dart SDK version. +LOCK_HASH=$(shasum -a 256 "$PROJECT_ROOT/pubspec.lock" | cut -d ' ' -f 1) +SDK_VER=$(dart --version 2>&1 | awk '{print $4}') +COMPILE_KEY="$LOCK_HASH:$SDK_VER" + +# 4. Determine staleness: binary missing, stamp empty/missing, stamp mismatch, +# or pubspec.yaml has been modified since the last successful build, or +# lib/app/_plugins.g.dart was regenerated by plugin:install or +# plugins:refresh. +# The pubspec.yaml-vs-STAMP_FILE comparison is correct because the stamp is +# written at the end of every successful compile; pubspec.yaml newer than +# the stamp means the user edited it after the last build. Comparing against +# pubspec.lock instead would trip on every freshly installed consumer +# because `dart pub add` updates pubspec.yaml after pub get writes the lock. +needs_build() { + [ ! -x "$BINARY" ] && return 0 + [ ! -s "$STAMP_FILE" ] && return 0 + [ "$(cat "$STAMP_FILE")" != "$COMPILE_KEY" ] && return 0 + [ "$PROJECT_ROOT/pubspec.yaml" -nt "$STAMP_FILE" ] && return 0 + [ "$PROJECT_ROOT/lib/app/_plugins.g.dart" -nt "$STAMP_FILE" ] && return 0 + return 1 +} + +# 5. Acquire the mkdir-based atomic lock. Trap-based release covers +# EXIT / INT / TERM, but SIGKILL (signal 9) bypasses traps and leaves the +# lock dir dangling. The PID-aware staleness check below recovers from +# that case: each holder writes its PID to $LOCK_DIR/pid; a competing +# acquirer probes that PID via `kill -0` and reclaims the lock when the +# owner is dead. POSIX sh; no flock / shlock / bash-isms. +acquire_lock() { + mkdir -p "$(dirname "$LOCK_DIR")" + while ! mkdir "$LOCK_DIR" 2>/dev/null; do + if [ -f "$LOCK_DIR/pid" ]; then + stored_pid="$(cat "$LOCK_DIR/pid" 2>/dev/null)" + if [ -n "$stored_pid" ] && ! kill -0 "$stored_pid" 2>/dev/null; then + # Owner is dead. Reclaim the lock dir and retry mkdir. + rm -rf "$LOCK_DIR" + continue + fi + fi + echo "fsa: waiting for another fsa invocation to finish..." >&2 + sleep 1 + done + echo $$ > "$LOCK_DIR/pid" +} + +if needs_build; then + acquire_lock + + # Release lock on exit, interrupt, or termination. + trap 'rm -rf "$LOCK_DIR" 2>/dev/null' EXIT INT TERM + + # 6. Re-check staleness inside the lock: another invocation may have built + # while we were waiting, making a second compile unnecessary. + if needs_build; then + T=$SECONDS + echo "fsa: building artisan CLI (one-time, ~5s)..." >&2 + + # 7. Compile to AOT binary via dart build cli. + if ! dart build cli -t "$ENTRY" -o "$CACHE_DIR" >&2; then + echo "fsa: dart build cli failed. Run 'dart analyze bin/dispatcher.dart' for details." >&2 + exit 1 + fi + + # 8. Write stamp atomically so a partial compile never leaves a stale stamp. + echo "$COMPILE_KEY" > "$STAMP_FILE.tmp" && mv "$STAMP_FILE.tmp" "$STAMP_FILE" + + echo "fsa: built in $((SECONDS - T))s" >&2 + fi +fi + +# 9. Exec into the compiled binary, replacing this shell process entirely. +exec "$BINARY" "$@" diff --git a/docs/component-registry.md b/docs/component-registry.md new file mode 100644 index 0000000..2026f8e --- /dev/null +++ b/docs/component-registry.md @@ -0,0 +1,510 @@ +--- +generated: manual (design:registry planned) +source: magic_starter generic component library +last_updated: 2026-06-25 +--- + +# Component Registry + +Machine-readable manifest of every component in the `magic_starter` generic component library. Maps each component to its variants, token bindings, and anti-patterns. + +> **design:registry note**: this file is intended to be generated and kept in sync by `make:component` and `previews:refresh`. Until that command emits it automatically, maintain it by hand when adding or modifying components. + +--- + +## Primitives + +Components backed by a Wind W-widget with no recipe layer. + +--- + +## Form Inputs + +### Button + +- **File**: `magic_starter/lib/src/ui/components/button/` +- **Class**: `Button` +- **Recipe**: `WindRecipe` in `button.recipe.dart` +- **Variants**: + - `intent`: `primary` | `secondary` | `ghost` | `destructive` + - `size`: `sm` | `md` | `lg` +- **Default variants**: `intent=primary`, `size=md` +- **Token bindings**: + - `primary`: `bg-primary text-on-primary` + - `secondary`: `bg-surface-container text-fg border border-color-border` + - `ghost`: `bg-transparent text-fg-muted` + - `destructive`: `bg-destructive text-on-destructive` + - `sm`: `text-xs px-3 py-1.5` + - `md`: `text-sm px-4 py-2` + - `lg`: `text-base px-6 py-3` +- **Anti-patterns**: + - Do not use more than one primary button per section. + - Do not use destructive intent outside confirm dialogs without a secondary confirmation step. + - Do not hardcode colors via `className` override when a variant covers the case. + +--- + +### Input + +- **File**: `magic_starter/lib/src/ui/components/input/` +- **Class**: `Input` +- **Recipe**: `WindRecipe` in `input.recipe.dart` +- **Variants**: + - `state`: `default` | `error` +- **Default variants**: `state=default` +- **Token bindings**: + - `default`: `bg-surface-container-high border border-color-border text-fg` + - `error`: `bg-surface-container-high border border-color-destructive text-fg` +- **Anti-patterns**: + - Do not render error state without an error message in the parent `FormField`. + - Do not use raw `WInput` directly; prefer `Input` so the recipe layer is consistent. + +--- + +### Textarea + +- **File**: `magic_starter/lib/src/ui/components/textarea/` +- **Class**: `Textarea` +- **Recipe**: `WindRecipe` in `textarea.recipe.dart` +- **Variants**: + - `state`: `default` | `error` +- **Default variants**: `state=default` +- **Token bindings**: same as Input. +- **Anti-patterns**: same as Input. + +--- + +### Checkbox + +- **File**: `magic_starter/lib/src/ui/components/checkbox/` +- **Class**: `Checkbox` +- **Recipe**: `WindRecipe` in `checkbox.recipe.dart` +- **Variants**: none (state is driven by `checked:` prefix) +- **Token bindings**: + - unchecked: `border-color-border bg-surface-container-high` + - checked (`checked:` state): `bg-primary border-primary` +- **Anti-patterns**: + - Do not use Material `Checkbox`; always use this component. + +--- + +### Switch + +- **File**: `magic_starter/lib/src/ui/components/switch/` +- **Class**: `Switch` +- **Recipe**: `WindRecipe` in `switch.recipe.dart` +- **Variants**: none (state is driven by `checked:` prefix on track/thumb) +- **Token bindings**: + - track off: `bg-surface-container border-color-border` + - track on (`checked:`): `bg-primary` + - thumb: `bg-surface` +- **Anti-patterns**: + - Do not use Material `Switch`. + - Do not animate thumb translate outside the Wind checked state prefix. + +--- + +### Radio + +- **File**: `magic_starter/lib/src/ui/components/radio/` +- **Class**: `Radio` +- **Generic type**: `Radio` +- **Recipe**: `WindRecipe` in `radio.recipe.dart` +- **Variants**: none (state is driven by `selected:` prefix) +- **Token bindings**: + - unselected: `border-color-border bg-surface-container-high` + - selected (`selected:`): `bg-primary border-primary` +- **Anti-patterns**: + - Do not use Material `Radio`. + - Group state management is the caller's responsibility (pass `groupValue`). + +--- + +## Display + +### Badge + +- **File**: `magic_starter/lib/src/ui/components/badge/` +- **Class**: `Badge` +- **Recipe**: `WindRecipe` in `badge.recipe.dart` +- **Variants**: + - `tone`: `neutral` | `primary` | `accent` | `success` | `warning` | `destructive` | `outline` +- **Default variants**: `tone=neutral` +- **Token bindings**: + - `neutral`: `bg-surface-container text-fg-muted` + - `primary`: `bg-primary-container text-primary` + - `accent`: `bg-accent text-on-primary` + - `success`: `bg-success text-on-primary` + - `warning`: `bg-warning text-on-primary` + - `destructive`: `bg-destructive-container text-destructive` + - `outline`: `bg-transparent text-fg border border-color-border` +- **Anti-patterns**: + - Do not use badges for interactive elements; they are display-only. + - Do not use raw hex to create a custom tone; add a new variant value instead. + +--- + +### Typography + +- **File**: `magic_starter/lib/src/ui/components/typography/` +- **Class**: `Typography` +- **Recipe**: `WindRecipe` in `typography.recipe.dart` +- **Variants**: + - `variant`: `h1` | `h2` | `h3` | `body` | `caption` +- **Default variants**: `variant=body` +- **Token bindings**: + - `h1`: `text-3xl font-bold text-fg leading-tight tracking-tight` + - `h2`: `text-2xl font-bold text-fg` + - `h3`: `text-xl font-semibold text-fg` + - `body`: `text-sm text-fg` + - `caption`: `text-xs text-fg-muted` +- **Anti-patterns**: + - Do not use raw `WText` for typographic content; use `Typography` so the scale is consistent. + - Semantics (h1/h2) are secondary to hierarchy; a section title can use `h2` even inside a card. + +--- + +### Skeleton + +- **File**: `magic_starter/lib/src/ui/components/skeleton/` +- **Class**: `Skeleton` +- **Recipe**: `WindRecipe` in `skeleton.recipe.dart` +- **Variants**: + - `shape`: `block` | `text` | `circle` +- **Default variants**: `shape=block` +- **Token bindings**: + - all shapes: `bg-surface-container-high motion-safe:animate-pulse` +- **Anti-patterns**: + - Use `Skeleton` instead of spinners for content loading states. + - Do not animate outside `motion-safe:` prefix (respect `disableAnimations`). + +--- + +## Card + +### Card (migrated from MagicStarterCard) + +- **File**: `magic_starter/lib/src/ui/components/card/` +- **Class**: `Card` +- **Enum**: `CardVariant` +- **Recipe**: `WindRecipe` in `card.recipe.dart` +- **Variants**: + - `tone`: `surface` | `inset` | `elevated` +- **Default variants**: `tone=surface` +- **Token bindings**: + - `surface`: `bg-surface-container border border-color-border` + - `inset`: `bg-surface-container-high` + - `elevated`: `bg-surface shadow-sm` +- **Slots**: `header`, `child` (body), `footer` +- **Anti-patterns**: + - Do not bake CardVariant logic into child components; pass `tone` to `Card` at the call site. + - Do not use `elevated` on dark backgrounds where shadow is invisible; prefer `surface` with a border. + +--- + +## Selection + +### Select + +- **File**: `magic_starter/lib/src/ui/components/select/` +- **Class**: `Select` +- **Recipe**: `WindSlotRecipe` in `select.recipe.dart` +- **Slots**: `trigger`, `popup`, `item` +- **Token bindings**: + - trigger: `bg-surface-container-high border border-color-border text-fg rounded-DEFAULT` + - popup: `bg-surface border border-color-border shadow-sm rounded-md` + - item: `text-sm text-fg hover:bg-surface-container-high` +- **Anti-patterns**: + - Do not use Material `DropdownButton`; use `Select`. + +--- + +### Combobox + +- **File**: `magic_starter/lib/src/ui/components/combobox/` +- **Class**: `Combobox` +- **Recipe**: `WindSlotRecipe` in `combobox.recipe.dart` +- **Slots**: `trigger`, `popup`, `item` +- **Token bindings**: same as Select, plus debounce search input. +- **Anti-patterns**: same as Select. + +--- + +### SegmentedControl + +- **File**: `magic_starter/lib/src/ui/components/segmented_control/` +- **Class**: `SegmentedControl` +- **Recipe**: `WindSlotRecipe` in `segmented_control.recipe.dart` +- **Variants**: + - `size`: `sm` | `md` +- **Slots**: `root`, `item` +- **Token bindings**: + - root: `bg-surface-container rounded-md p-0.5` + - item active (`selected:`): `bg-surface text-fg shadow-sm rounded-sm` + - item inactive: `text-fg-muted` +- **Anti-patterns**: + - Do not use for more than 4-5 options; use `Tabs` or a `Select` instead. + +--- + +### Tabs + +- **File**: `magic_starter/lib/src/ui/components/tabs/` +- **Class**: `Tabs` +- **Recipe**: `WindSlotRecipe` in `tabs.recipe.dart` +- **Slots**: `list`, `tab`, `panel` +- **Token bindings**: + - list: `border-b border-color-border` + - tab inactive: `text-fg-muted` + - tab active (`selected:`): `text-primary border-b-2 border-primary` + - panel: `pt-4` +- **Anti-patterns**: + - Do not use Material `TabBar`; use `Tabs`. + +--- + +### Accordion + +- **File**: `magic_starter/lib/src/ui/components/accordion/` +- **Class**: `Accordion` +- **Recipe**: `WindSlotRecipe` in `accordion.recipe.dart` +- **Slots**: `root`, `item`, `header`, `trigger`, `panel` +- **Token bindings**: + - root: `border border-color-border rounded-md divide-y divide-color-border` + - trigger: `text-fg font-medium` + - panel: `text-fg-muted text-sm px-4 pb-4` +- **Anti-patterns**: + - Do not use for top-level navigation; use for secondary content disclosure only. + +--- + +## Overlays + +### Dialog + +- **File**: `magic_starter/lib/src/ui/components/dialog/` +- **Class**: `Dialog` +- **Recipe**: `WindSlotRecipe` in `dialog.recipe.dart` +- **Slots**: `backdrop`, `panel`, `title`, `footer` +- **Token bindings**: + - backdrop: `bg-fg/50` (semi-transparent fg overlay) + - panel: `bg-surface rounded-lg shadow-xl max-w-md w-full` + - title: `text-fg font-semibold text-lg` + - footer: `flex gap-3 justify-end pt-4` +- **Anti-patterns**: + - Always use `Dialog.show()` static factory; do not push dialogs as routes. + - Keep dialog content focused; avoid multi-step flows inside a single dialog. + +--- + +### ConfirmDialog + +- **File**: `magic_starter/lib/src/ui/components/confirm_dialog/` +- **Class**: `ConfirmDialog` +- **Enum**: `ConfirmDialogVariant` +- **Recipe**: `WindSlotRecipe` in `confirm_dialog.recipe.dart` +- **Variants**: + - `variant`: `primary` | `danger` | `warning` +- **Token bindings**: + - `danger`: confirm button uses `Button(intent: ButtonIntent.destructive)` + - `warning`: confirm button uses `Button(intent: ButtonIntent.secondary)` with warning badge + - `primary`: confirm button uses `Button(intent: ButtonIntent.primary)` +- **Anti-patterns**: + - Use `danger` for irreversible destructive actions only (account deletion, data wipe). + - Do not use `warning` for routine confirmation; reserve it for significant but reversible changes. + +--- + +### BottomSheet + +- **File**: `magic_starter/lib/src/ui/components/bottom_sheet/` +- **Class**: `BottomSheet` +- **Recipe**: `WindSlotRecipe` in `bottom_sheet.recipe.dart` +- **Slots**: `backdrop`, `panel`, `handle`, `title`, `footer` +- **Token bindings**: + - panel: `bg-surface rounded-t-xl` + - handle: `bg-surface-container-high rounded-full` +- **Anti-patterns**: + - Respect `SafeArea` at the bottom for home indicator. + - Do not embed complex multi-step flows; keep to contextual actions. + +--- + +### Toast + +- **File**: `magic_starter/lib/src/ui/components/toast/` +- **Class**: `Toast` +- **Recipe**: `WindRecipe` in `toast.recipe.dart` +- **Variants**: + - `tone`: `neutral` | `success` | `warning` | `destructive` +- **Token bindings**: + - `neutral`: `bg-surface border border-color-border text-fg` + - `success`: `bg-success text-on-primary` + - `warning`: `bg-warning text-on-primary` + - `destructive`: `bg-destructive text-on-destructive` +- **Anti-patterns**: + - Use for non-critical feedback only; critical errors belong in a dialog or inline error state. + - Auto-dismiss after 4-6 seconds unless action is required. + +--- + +### Tooltip + +- **File**: `magic_starter/lib/src/ui/components/tooltip/` +- **Class**: `Tooltip` +- **Recipe**: `WindSlotRecipe` in `tooltip.recipe.dart` +- **Slots**: `trigger`, `content` +- **Token bindings**: + - content: `bg-fg text-surface text-xs rounded-md px-2 py-1` +- **Anti-patterns**: + - Do not use tooltips for essential information; they are invisible on touch devices. + - WPopover real-click dismiss race is a known issue; do not add Tooltip to interactive paths that require precise tap timing. + +--- + +### DropdownMenu + +- **File**: `magic_starter/lib/src/ui/components/dropdown_menu/` +- **Class**: `DropdownMenu` +- **Recipe**: `WindSlotRecipe` in `dropdown_menu.recipe.dart` +- **Slots**: `trigger`, `panel`, `item`, `separator` +- **Token bindings**: + - panel: `bg-surface border border-color-border rounded-md shadow-sm` + - item: `text-sm text-fg hover:bg-surface-container-high` + - separator: `border-t border-color-border my-1` +- **Anti-patterns**: + - Do not use for primary navigation (use `Navbar` or `Tabs`). + - WPopover real-click dismiss race is a known issue; do not regress dismiss behavior. + +--- + +## Structure + +### FormField + +- **File**: `magic_starter/lib/src/ui/components/form_field/` +- **Class**: `FormField` (exported as `MagicFormField` to avoid collision with Flutter's `FormField`) +- **Recipe**: `WindSlotRecipe` in `form_field.recipe.dart` +- **Slots**: `root`, `label`, `hint`, `error` +- **Token bindings**: + - root: `flex flex-col gap-1` + - label: `text-sm font-medium text-fg` + - hint: `text-xs text-fg-muted` + - error: `text-xs text-destructive` +- **Anti-patterns**: + - Always wrap `Input`/`Textarea` in `MagicFormField`; never render label/error inline. + - Import as `MagicFormField` to avoid collision with Flutter's `FormField` widget. + +--- + +### PageHeader + +- **File**: `magic_starter/lib/src/ui/components/page_header/` +- **Class**: `PageHeader` +- **Recipe**: `WindSlotRecipe` in `page_header.recipe.dart` +- **Slots**: `title`, `subtitle`, `leading`, `actions`, `inlineActions` +- **Token bindings**: + - title: `text-xl font-bold text-fg` + - subtitle: `text-sm text-fg-muted` +- **Anti-patterns**: + - Do not add navigation chrome inside `PageHeader`; it is a content title, not an app bar. + +--- + +### EmptyState + +- **File**: `magic_starter/lib/src/ui/components/empty_state/` +- **Class**: `EmptyState` +- **Recipe**: `WindSlotRecipe` in `empty_state.recipe.dart` +- **Slots**: `root`, `iconWrap`, `title`, `description`, `action` +- **Token bindings**: + - iconWrap: `text-fg-disabled` + - title: `text-fg font-semibold text-lg` + - description: `text-fg-muted text-sm` +- **Anti-patterns**: + - Always include a call-to-action in the `action` slot; an empty state without an action is a dead end. + - Hide filters, tabs, or sorting controls that do not apply when the list is empty. + +--- + +### ErrorState + +- **File**: `magic_starter/lib/src/ui/components/error_state/` +- **Class**: `ErrorState` +- **Recipe**: `WindSlotRecipe` in `error_state.recipe.dart` +- **Slots**: `root`, `iconWrap`, `title`, `description`, `action` +- **Token bindings**: + - iconWrap: `text-destructive` + - title: `text-red-700 dark:text-red-400 font-semibold text-lg` + - description: `text-fg-muted text-sm` +- **Anti-patterns**: + - Use for unrecoverable states; for recoverable network errors, show a retry button in the `action` slot. + +--- + +### Navbar + +- **File**: `magic_starter/lib/src/ui/components/navbar/` +- **Class**: `Navbar` +- **Recipe**: `WindSlotRecipe` in `navbar.recipe.dart` +- **Slots**: `root`, `item`, `activeItem` +- **Token bindings**: + - root: `bg-surface border-t border-color-border` + - item inactive: `text-fg-muted` + - item active (`selected:`): `text-primary` +- **Anti-patterns**: + - Limit to 3-5 primary destinations. + - Do not place secondary actions in the bottom nav; use `DropdownMenu` or a settings page. + +--- + +## Composites + +### SocialDivider + +- **File**: `magic_starter/lib/src/ui/components/social_divider/` +- **Class**: `SocialDivider` +- **Token bindings**: `border-color-border text-fg-muted` +- **Anti-patterns**: + - Use only on auth screens to separate email login from social login options. + +--- + +### NotificationDropdown + +- **File**: Composite consuming `DropdownMenu` + `Badge` +- **Token bindings**: inherits from composites. +- **Anti-patterns**: + - Do not change the `StreamBuilder` unread-count subscription pattern; it is intentional. + +--- + +### UserProfileDropdown + +- **File**: Composite consuming `DropdownMenu` +- **Anti-patterns**: + - Do not add business logic to the dropdown; route to profile/settings views. + +--- + +### TeamSelector + +- **File**: Composite consuming `Select` or `DropdownMenu` +- **Anti-patterns**: + - Keep team-switch callback through `teamResolver`; do not hard-wire team ID. + +--- + +## Anti-patterns (global) + +| Anti-pattern | Category | Fix | +|-------------|----------|-----| +| Raw `Color(0xFF...)` or `Colors.*` in recipe or widget | Token violation | Use semantic alias (e.g. `bg-primary`) | +| Hardcoded pixel margin (`SizedBox(height: 13)`) | Spacing violation | Use Wind spacing utilities on the 4px scale | +| Multiple preview classes in one file | Preview structure | One `*.preview.dart` per component | +| Exporting preview class from `index.dart` | Preview boundary | `previews:refresh` discovers `*.preview.dart` directly | +| Importing `package:fluttersdk_wind/src/...` directly | Import convention | Use `package:magic/magic.dart` (re-exports wind) | +| Using Material `Switch`, `Checkbox`, `Radio`, `TabBar` | Primitive collision | Use the project component equivalents | +| CSS-only Wind utilities (`box-shadow`, `filter`, `transform`) | Wind unsupported | Use Flutter animation APIs | +| `Icons.*` inline in widget body | Tree-shaking | Extract as `static const IconData _icon = Icons.x;` | +| Missing `dark:` on any color token | Dark parity | Every alias expands to a light+dark pair | diff --git a/docs/design-culture/accessibility-wcag.md b/docs/design-culture/accessibility-wcag.md new file mode 100644 index 0000000..d914fe4 --- /dev/null +++ b/docs/design-culture/accessibility-wcag.md @@ -0,0 +1,201 @@ +# Accessibility / WCAG (Flutter/Wind reference) + +Read this for every screen. Accessibility is a constraint that applies regardless of design +language, and it is legally required in most markets. Target WCAG 2.2 Level AA. + +## What to target + +WCAG 2.2 AA is the baseline (EN 301 549 / EU Accessibility Act, ADA Title II). WCAG 3.0 is a +draft; do not design to APCA for compliance yet. + +## Contrast (hard numbers) + +- Body text: **4.5:1** against its background. +- Large text (>=24px, or >=18.66px bold): **3:1**. +- UI components and meaningful graphics (borders, icons, input outlines, focus rings): **3:1** + against adjacent color. +- Check BOTH light and dark independently; a token that passes in light can fail in dark. + +### design:lint enforcement + +`dart run bin/dispatcher.dart design:lint` enforces the 4.5:1 ratio on every `on-X`/`X` role +pair defined in DESIGN.md. It implements the WCAG relative-luminance formula (sRGB channel +linearization, 4.5:1 ratio check) against both the light and dark hex values. A lint failure +means the token pair does not pass for normal body text. + +The passing pairs in the current DESIGN.md: + +- `text-on-primary` (#FFFFFF) on `bg-primary` (#7C3AED light): 5.7:1 (pass) +- `text-on-destructive` (#FFFFFF) on `bg-destructive` (#DC2626 light): 4.8:1 (pass) + +If you update any color in DESIGN.md, re-run `design:lint` to verify the pair still passes in +both themes. + +## Touch targets + +- WCAG 2.5.8 (AA): interactive targets at least **24x24 logical px**, or enough spacing so a + 24px circle on each does not overlap a neighbor. +- Apple HIG and Material 3 ask for 44px and 48px respectively; those are the practical targets. +- In Wind className: `min-h-11` (44px) clears both Apple and the WCAG floor. Button `md`/`lg` + already meet this. Keep custom controls at `min-h-11`. + +## Focus visibility + +- WCAG 2.4.11 (AA): a focused element must not be fully hidden by sticky headers or overlays. + In Flutter: use `Scrollable.ensureVisible` or `ScrollController` to scroll focused elements + into view when a sticky header is present. +- Focus ring: at least 2px perimeter with 3:1 contrast against the unfocused state. +- In Wind className: `focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2` + provides a compliant focus ring. Apply it to every custom interactive widget. +- Never use a Flutter `FocusNode` that suppresses the default focus indicator without a + visible replacement. + +## Flutter Semantics + +Flutter does not use HTML; screen readers (TalkBack, VoiceOver) read the Semantics tree. Every +rule below maps the HTML/ARIA concept to Flutter. + +### Every interactive widget needs a Semantics label + +```dart +// Icon-only button +Semantics( + label: 'Close dialog', + button: true, + child: WButton(onPressed: onClose, child: Icon(Icons.close)), +) + +// Image with meaning +Semantics( + label: 'Profile photo for Jane Smith', + image: true, + child: CircleAvatar(backgroundImage: ...), +) + +// Decorative image: excludeSemantics: true +ExcludeSemantics(child: decorativeIcon) +``` + +### Heading hierarchy + +Use `Semantics(header: true)` on screen-level headings. Maintain one primary heading per screen. +Do not use heading markup for visual sizing; use `Typography` variants for visual size and +`Semantics(header: true)` for semantic role. + +```dart +Semantics( + header: true, + child: Typography(variant: TypographyVariant.headlineLg, text: 'Profile settings'), +) +``` + +### Form fields + +Every input must have a visible, persistent label. The `FormField` component handles label +association; do not rely on placeholder text as the label (it disappears on focus). + +Error messages must be text, not color alone. Use the `FormField` `errorText` parameter; it +renders red text AND associates it with the field via `Semantics(liveRegion: true)`. + +```dart +FormField( + label: 'Email address', + errorText: controller.errors['email'], + child: Input(form: form, name: 'email'), +) +``` + +### Live regions for dynamic updates + +Toast notifications and async status updates must announce themselves to screen readers: + +```dart +Semantics( + liveRegion: true, + child: Toast(message: 'Profile saved successfully.'), +) +``` + +For urgent alerts use `Semantics(liveRegion: true, namesRoute: false)` with a high-priority +announcement. Do not use live regions for every state change; only for content the user would +otherwise miss. + +### Navigation landmarks + +Wrap the main content area in `Semantics(explicitChildNodes: true)` to preserve tree structure. +The `AppLayout` shell handles landmark-level semantics; do not add redundant wrappers. + +## Color independence + +Never convey information by color alone (WCAG 1.4.1). Pair every color signal with text, an +icon, or a pattern: + +- An error input field gets `bg-destructive-container` border AND an error message text AND an + error icon. +- A success badge has `bg-success` background AND a checkmark icon AND a label. +- Active nav items use `bg-primary-container` AND bold weight AND an active indicator line. + +## Reduced motion + +Respect the OS "Reduce Motion" setting: + +```dart +// In a widget build method +final reduceMotion = MediaQuery.of(context).disableAnimations; + +// In Wind className: motion-safe: prefix gates the animation +WButton(className: 'transition-colors motion-safe:active:scale-95 ...') +``` + +Under reduced motion: disable parallax, large translates/scales, looping autoplay, staggered +reveals, and spinner animations. Keep or substitute: opacity fades, color transitions. Do not +delete motion that conveys state; substitute a static equivalent. + +No content flashes more than 3 times per second. Auto-playing motion over 5 seconds needs a +pause control. + +## Dragging and gestures + +Every drag interaction (sortable lists, sliders, swipe-to-dismiss) needs a single-pointer +non-drag alternative (WCAG 2.5.7 AA). For swipe-to-dismiss: also show a button or +long-press-menu option to trigger the same action. + +## Design-time checklist + +Before marking any screen done: + +1. Text contrast >=4.5:1 (3:1 large), verified in both light and dark via `design:lint`. +2. UI/border/focus-ring contrast >=3:1. +3. No color-only signals: every state has a text or icon companion. +4. Touch targets >=44px (min-h-11) for all interactive elements. +5. Visible focus ring (`focus-visible:ring-2 focus-visible:ring-primary`) on every interactive + widget; never obscured by sticky chrome. +6. One primary screen heading with `Semantics(header: true)`. +7. Every input has a visible label via `FormField`; errors are text + associated. +8. Decorative images have `ExcludeSemantics`; informative images have a `Semantics(label: ...)`. +9. All interactive elements keyboard/switch-accessible; no widget swallows focus without release. +10. Non-essential motion gated behind `motion-safe:` or `!MediaQuery.of(context).disableAnimations`. + +## How to apply in this codebase + +- Tokens pass WCAG: `text-fg` on `bg-surface`, `text-on-primary` on `bg-primary`, + `text-on-destructive` on `bg-destructive` are designed to pass. Re-check any custom pair. +- `FormField` component handles label association and error text automatically. +- `WButton`, `WInput`, `WAnchor` ship with a focus-ring className. Keep it; never strip it. +- Run `dart run bin/dispatcher.dart design:lint` after every DESIGN.md color change. +- Use `ExcludeSemantics` for decorative icons; `Semantics(label: ...)` for meaningful ones. + +## See also + +- [DESIGN.md](../DESIGN.md): color values; run design:lint to verify pairs +- [refactoring-ui.md](refactoring-ui.md): color independence, hierarchy, empty states +- [motion-interaction.md](motion-interaction.md): reduced-motion patterns +- [wind-responsive.md](wind-responsive.md): touch targets, SafeArea, hit-target sizing + +## Sources + +- W3C WAI: WCAG 2.2 Recommendation (1.4.1, 1.4.3, 1.4.11, 2.1.1, 2.4.3, 2.4.7, 2.4.11, 2.5.7, + 2.5.8, 3.3.1-3.3.3). +- Flutter documentation: Semantics, ExcludeSemantics, MediaQuery.disableAnimations, + FocusableActionDetector. +- EN 301 549 / EU Accessibility Act; ADA Title II. diff --git a/docs/design-culture/apple-hig.md b/docs/design-culture/apple-hig.md new file mode 100644 index 0000000..6caf2fa --- /dev/null +++ b/docs/design-culture/apple-hig.md @@ -0,0 +1,134 @@ +# Apple Human Interface Guidelines (Flutter/Wind reference) + +Read this before building a screen that should feel native on Apple platforms. It is a decision tool: +pick the language, then apply the rules. The guidance below is adapted from HIG principles to +Flutter/Wind/Magic idioms. + +## When to choose this language + +Choose Apple HIG when: + +- The audience is Apple-ecosystem-native and expects iOS/iPadOS patterns (bottom tab bar, swipe-back, edge gestures). +- The product is content-first: chrome should recede so content leads. +- Calm, restrained minimalism fits the brand better than loud expression. + +Avoid it when: + +- The product must reach Android parity equally (use material-design-3 instead). +- The domain is data-dense or tool-heavy; Material's explicit density fits better. +- The brand needs expressive, decorative visuals. + +## Core principles + +- **Clarity**: legible text at every size, precise icons, subtle adornment. +- **Deference**: the UI helps users interact with content without competing with it. +- **Depth**: layers communicate hierarchy via tonal elevation, not heavy drop shadows. + +## Layout and spacing rules + +- Every interactive element needs a minimum 44x44 logical-px hit target. On Flutter this means + `InkWell`, `GestureDetector`, or `WButton` with `min-h-11 min-w-11` in the className. Pad + invisibly rather than shrink the control. +- Use generous whitespace. Wind spacing follows the 4px logical scale; prefer `p-4`/`p-6`/`gap-4` + and resist `p-2` on content areas. +- On narrow (mobile) widths use `p-4` (16px) screen-edge margins; on tablet/desktop widths use `p-5` + (20px) or the layout shell's `md:px-5`. Do not reuse one margin across all breakpoints. +- Reflow vertically at narrow widths rather than truncate. +- Derive nested corner radii concentrically: inner radius = parent radius minus padding. In Wind + terms: a card with `rounded-lg` (16px) around content with 12px padding gets `rounded-md` (12px) + on a nested control, not an arbitrary value. + +In `AppLayout`, the `sm:` and `md:` breakpoint prefixes (`sm:flex-row`, `md:hidden`) handle the +compact-to-regular reflow. See [wind-responsive.md](wind-responsive.md) for the full breakpoint map. + +## Typography + +- Let type carry hierarchy: bolder, left-aligned section headings. Keep one font family; the + DESIGN.md typography font is authoritative (Inter by default in magic_example). +- Use the Typography component variants rather than arbitrary sizes: + `headline-lg` for page titles, `title-lg` for section heads, `body-lg`/`body-md` for content, + `label-md`/`label-sm` for interactive labels and captions. +- Body weight 400, headings and interactive labels weight 600-700. Never below 400 for small text. +- Left-align body text; never center multi-line prose. + +## Color and material + +- Use semantic tokens only. Never raw hex inside a component. + - Page canvas: `bg-surface` + - Cards and sheets: `bg-surface-container` + - Primary action: `bg-primary text-on-primary` + - Destructive: `bg-destructive text-on-destructive` + - Body text: `text-fg` + - Secondary text: `text-fg-muted` + - Hairlines: `border-color-border` + + The 17 semantic alias keys are defined in `DESIGN.md` and applied as wind aliases. Dark-mode + pairs are bundled in each token: `bg-surface` resolves to `bg-[#FFFFFF] dark:bg-[#030712]` + automatically. + +- Tint, do not repaint: apply `bg-primary` to primary actions only. Keep chrome neutral + (`bg-surface-container`, `text-fg`). +- In dark mode, elevation goes lighter (surface-container is lighter than surface). Never use + pure black; the `surface` dark token is `#030712`, an elevated dark gray. +- Reserve translucency for the navigation layer only. In Flutter, approximate a glass nav bar + with `BackdropFilter` + a semi-transparent surface. Never glass-on-glass, never glass in the + content layer. + +## Motion (Apple-specific feel) + +See [motion-interaction.md](motion-interaction.md) for Flutter easing/duration mechanics. + +Apple-specific feel: + +- Transitions originate from the element that triggered them (a sheet slides up from the + triggering button's area, not from a random edge). +- Use spring-based or ease-out curves; keep micro-interactions under 300ms. +- Animate state transitions that communicate hierarchy, not frequent interactions. +- Wrap non-essential animations with `motion-safe:` in Wind className, or guard with + `MediaQuery.of(context).disableAnimations` in Flutter widget code. + +## Accessibility + +- Contrast: 4.5:1 for normal text, 3:1 for large text and UI components, in BOTH light and dark. + `design:lint` enforces 4.5:1 on every `on-X`/`X` role pair. See + [accessibility-wcag.md](accessibility-wcag.md). +- Support text scaling: never hardcode font sizes for primary content. The Typography component + uses logical px; Flutter's `textScaleFactor` respects the OS setting automatically. +- Respect Reduce Motion: substitute a crossfade or opacity fade, do not delete animations. + Use `MediaQuery.of(context).disableAnimations` or the `motion-safe:` Wind prefix. +- Never use color as the only signal. Pair every color-only state with an icon or text label. +- Provide `Semantics` labels on icon-only controls: + ```dart + Semantics(label: 'Close', child: WButton(onPressed: ..., child: Icon(Icons.close))) + ``` + +## What makes it feel authentically Apple + +Reproduce: restraint (one primary surface per view), generous whitespace, crisp single-family +type, semantic adaptive color, subtle depth via tonal elevation. Avoid the tells of a cheap +imitation: raw hex, fixed text sizes, multiple typefaces, red used for non-destructive actions, +dense cramped layouts. + +## How to apply in this codebase + +1. Map the type scale to `Typography` component variants; Inter is the default app font. +2. Use semantic tokens exclusively in wind `className`: `bg-surface text-fg`, + `bg-primary text-on-primary`, `border-color-border`. +3. Dark mode is automatic: each token carries its `dark:` pair. Verify both themes pass contrast. +4. Honor 44px targets: Button `md`/`lg` already clear it; keep custom controls at `min-h-11`. +5. Keep radii concentric: `rounded-lg` (16px) outer card -> `rounded-md` (12px) nested controls. +6. Reserve `bg-destructive` for genuinely destructive actions, matching Apple's reserved-red rule. + +## See also + +- [DESIGN.md](../DESIGN.md): the 17 semantic token definitions and color values for this app +- [wind-responsive.md](wind-responsive.md): breakpoints, PageContainer, safe-area, sidebar vs bottom nav +- [accessibility-wcag.md](accessibility-wcag.md): contrast requirements and design:lint enforcement +- [motion-interaction.md](motion-interaction.md): easing, duration, and reduced-motion patterns in Flutter +- [material-design-3.md](material-design-3.md): M3 alternative when Material feel is preferred + +## Sources + +- Apple HIG: Design Principles, Layout, Typography, Color, Motion, Accessibility + (developer.apple.com/design/human-interface-guidelines). +- Flutter documentation: Semantics, MediaQuery.disableAnimations, TextScaleFactor. diff --git a/docs/design-culture/material-design-3.md b/docs/design-culture/material-design-3.md new file mode 100644 index 0000000..0ab4436 --- /dev/null +++ b/docs/design-culture/material-design-3.md @@ -0,0 +1,216 @@ +# Material Design 3 / Material You (Flutter/Wind reference) + +Read this before building a screen in Google's design language. It is a decision tool first, +spec second. This doc maps M3 concepts onto Flutter's `ThemeData`/`ColorScheme` and the Wind +semantic tokens defined in [DESIGN.md](../DESIGN.md). + +## When to choose this language + +Choose Material when: + +- Building for Android / Wear OS, or cross-platform apps that should feel at home on Google + devices. +- The domain is data-dense or tool-heavy (dashboards, forms, settings): Material's explicit + density and component set fit. +- You need robust light/dark plus accessible contrast without per-component tuning; the role + system gives it for free. +- The brand tolerates a colorful, rounded, springy aesthetic. + +Avoid it when: + +- You want a restrained, content-first Apple feel; see [apple-hig.md](apple-hig.md). + +## Core idea + +Material 3 is token-driven: components reference semantic ROLES, never raw hex. A single seed +color generates tonal palettes via the HCT color space; roles map specific tones to slots and +remap for dark mode automatically. + +In Flutter this is `ColorScheme.fromSeed(seedColor: ...)` plus `ThemeData.colorScheme`. In this +codebase, `design:sync` generates a `WindThemeData` from `DESIGN.md`; the violet `primary` +seed drives `toThemeData()` for Material interop. See `lib/config/` for the generated theme. + +## Mapping M3 roles to the 17 Wind semantic tokens + +The 17 semantic alias keys in DESIGN.md align to M3 roles. Use the Wind token in `className`; +Flutter's `ColorScheme` resolves the equivalent role for Material widget sub-trees. + +| M3 role | Wind token | Usage | +|---|---|---| +| `surface` | `bg-surface` | Page canvas | +| `surface-container` | `bg-surface-container` | Cards, sheets | +| `surface-container-high` | `bg-surface-container-high` | Input backgrounds, nested panels | +| `on-surface` | `text-fg` | Primary body text | +| `on-surface-variant` | `text-fg-muted` | Secondary/helper text | +| (disabled) | `text-fg-disabled` | Disabled labels | +| `primary` | `bg-primary` | Primary action fills | +| `on-primary` | `text-on-primary` | Text on primary fills | +| `primary-container` | `bg-primary-container` | Tonal button fills, badge backgrounds | +| (secondary accent) | `bg-accent` | Secondary emphasis fills | +| `outline` | `border-color-border` | Interactive borders, input outlines | +| `outline-variant` | `border-color-border-subtle` | Decorative dividers | +| `error` | `bg-destructive` | Error/danger fills | +| `on-error` | `text-on-destructive` | Text on error fills | +| `error-container` | `bg-destructive-container` | Error container backgrounds | +| (success) | `bg-success` | Success state fills | +| (warning) | `bg-warning` | Warning state fills | + +Always pair a role with its `on-*` token (for example `bg-primary text-on-primary`). Mixing +families breaks the guaranteed contrast. + +### Using tokens in Wind className + +```dart +// Primary action button +Button( + className: 'bg-primary text-on-primary rounded-md px-4 py-3', + child: Text('Save'), +) + +// Card surface +WCard( + className: 'bg-surface-container rounded-lg p-4 border border-color-border', + child: ..., +) + +// Secondary text +WText('Helper text', className: 'text-fg-muted text-sm') +``` + +Dark mode pairs are bundled: `bg-surface-container` resolves to +`bg-[#F9FAFB] dark:bg-[#111827]` automatically via wind aliases. + +## Surface depth system + +Express elevation with tonal surface-container steps, not drop shadows. + +``` +surface (#FFFFFF) <- page canvas + surface-container (#F9FAFB) <- cards, sheets + surface-container-high (#F3F4F6) <- input backgrounds, nested panels +``` + +Dark mode steps invert correctly: deeper grays for lower-elevation surfaces. Reserve shadows +(`shadow-sm`, `shadow-md`) for genuinely floating elements (popovers, modals). + +## State layers + +Hover, focus, and pressed states overlay the `on-*` color at low opacity: + +| State | Opacity | +|---|---| +| Hover | 8% | +| Focus | 10% | +| Pressed | 10% | +| Dragged | 16% | + +In Wind/magic_starter components this is expressed as `hover:bg-surface-container` or +`hover:opacity-90` rather than precise opacity math. Only one state layer at a time. + +## Navigation by breakpoint + +Material specifies navigation component by viewport width: + +- Under 600dp: bottom navigation bar (3-5 destinations) +- 600-839dp: navigation rail +- 840dp and above: navigation rail or navigation drawer + +In `AppLayout` (`magic_starter`) this maps to: + +- Mobile (below `md` breakpoint, 768px): bottom nav bar + hamburger drawer +- Desktop (`md:` and above): sidebar navigation rail + +Use `md:hidden` to show/hide between mobile and desktop layouts. See +[wind-responsive.md](wind-responsive.md) for the breakpoint map and layout patterns. + +## Typography + +The type scale below maps M3 roles to DESIGN.md typography variants. Use the `Typography` +component variant rather than specifying `text-*` sizes directly. + +| M3 role | Typography variant | Suggested className | +|---|---|---| +| Display Large | display | `font-bold` | +| Headline Large | headline-lg | `font-bold` | +| Headline Medium | headline-md | `font-semibold` | +| Title Large | title-lg | `font-semibold` | +| Body Large | body-lg | `font-normal` | +| Body Medium | body-md | `font-normal` | +| Label Medium | label-md | `font-semibold` | +| Label Small | label-sm | `font-medium` | + +Use weight 500-600 for interactive labels and tab titles, not Body weight. The DESIGN.md font +(Inter) is authoritative; do not override it per-component. + +## Shape scale + +| M3 name | Corner radius | Wind token | Use | +|---|---|---|---| +| Extra-small | 4px | `rounded-sm` | Chips, badges | +| Small | 8px | `rounded` | Inputs | +| Medium | 12px | `rounded-md` | Buttons | +| Large | 16px | `rounded-lg` | Cards, dialogs | +| Extra-large | 24px | `rounded-xl` | Bottom sheets | +| Full | 9999px | `rounded-full` | Pills, avatars | + +Use `rounded-lg` (16px) for cards and dialogs, `rounded-full` for pill badges, `rounded-md` +for buttons. Do not mix shape families without concentric nesting intent. + +## Component rules + +- One filled button per action group (the primary action). Secondary actions use tonal, outlined, + or text variants. Never two filled buttons side by side. +- Cards: `surface-container` background, `rounded-lg`, no shadow unless floating. Cards are + not navigation targets by themselves; wrap in a `GestureDetector` for interactivity. +- FAB or prominent action button: persists during scroll for the single most important action. + Not for destructive or rare actions. + +## Motion + +M3 uses spring-physics on Android/Compose. In Flutter, the closest equivalent is a +`CurvedAnimation` with `Curves.easeOutCubic` (entering) or `Curves.easeInCubic` (exiting). See +[motion-interaction.md](motion-interaction.md) for durations, reduced-motion patterns, and the +Wind `motion-safe:` prefix. + +M3 easing reference (use as curve approximations in Flutter `AnimationController`): + +- Entering: `emphasized-decelerate` ~ `cubic-bezier(0.05, 0.7, 0.1, 1)` -> `Curves.easeOutCubic` +- Exiting: `emphasized-accelerate` ~ `cubic-bezier(0.3, 0, 0.8, 0.15)` -> `Curves.easeInCubic` +- Standard: `cubic-bezier(0.2, 0, 0, 1)` -> `Curves.easeInOutCubic` + +## Accessibility + +- Touch targets: minimum 48x48dp (Material spec); WCAG floor is 24px. The Wind `min-h-11` + class (44px) meets both. Keep custom controls at `min-h-12` (48px) when targeting Material. +- The role system guarantees >=3:1 on `on-*` pairings for large text. Body text still needs + 4.5:1; `design:lint` enforces this on all `on-X`/`X` pairs. See + [accessibility-wcag.md](accessibility-wcag.md). +- Components must ship a visible focus ring. In Flutter use `FocusableActionDetector` or rely + on the `WButton`/`WInput` focus-ring className (`focus-visible:ring-2 focus-visible:ring-primary`). +- Provide `Semantics` for non-text content. Never use color alone. + +## How to apply in this codebase + +1. Use semantic tokens exclusively: never raw hex, never `Colors.*` constants in className. +2. Express depth by stepping `bg-surface` -> `bg-surface-container` -> `bg-surface-container-high`, + not by adding shadows. +3. Map the type scale to `Typography` component variants; weight 500-600 for interactive labels. +4. Keep radii from the shape scale; `rounded-lg` for cards, `rounded-md` for buttons, + `rounded-full` for pills. +5. For Material widget sub-trees (if used), the generated `ThemeData` from `design:sync` wires + the `ColorScheme` automatically. Do not override `ThemeData.colorScheme` by hand. + +## See also + +- [DESIGN.md](../DESIGN.md): the 17 semantic token definitions, hex values, and component tokens +- [wind-responsive.md](wind-responsive.md): breakpoints and navigation layout patterns +- [accessibility-wcag.md](accessibility-wcag.md): contrast requirements and design:lint enforcement +- [refactoring-ui.md](refactoring-ui.md): craft: hierarchy, spacing, type, color, and depth polish +- [motion-interaction.md](motion-interaction.md): easing, duration, and reduced-motion in Flutter + +## Sources + +- m3.material.io: color/roles, styles/typography, styles/shape, styles/motion/easing-and-duration, + components, foundations/accessible-design. +- Flutter documentation: ColorScheme.fromSeed, ThemeData, AnimationController, CurvedAnimation. +- material-foundation/material-color-utilities (HCT, tonal palette generation). diff --git a/docs/design-culture/motion-interaction.md b/docs/design-culture/motion-interaction.md new file mode 100644 index 0000000..4a35384 --- /dev/null +++ b/docs/design-culture/motion-interaction.md @@ -0,0 +1,214 @@ +# Motion and interaction (Flutter/Wind reference) + +Read this when adding any animation or interactive feedback to a screen. The 2026 consensus is +restraint: subtle, fast, purposeful. Motion you notice is usually wrong. + +## When to animate (and when not) + +Animate only for a reason: feedback (confirm an action), continuity (connect states), spatial +orientation (where did this come from), or guiding attention. Everything else is decoration; cut it. + +Frequency rule: the more often an action repeats, the less it should animate. + +| Frequency | Guideline | +|---|---| +| 100+ per day (toggles, tab switches) | No animation, instant | +| Tens per day (hover, nav item tap) | Minimal, <=150ms | +| Occasional (modals, drawers, toasts) | Standard animation | +| Rare / first-run (onboarding, empty state) | A little delight is allowed | + +## Easing + +In Flutter, easing maps to `Curve` values in `CurvedAnimation`: + +| Intent | Flutter Curve | Approximate bezier | +|---|---|---| +| Element entering | `Curves.easeOut` | cubic-bezier(0.23, 1, 0.32, 1) | +| Material entering | `Curves.easeOutCubic` | cubic-bezier(0.05, 0.7, 0.1, 1) | +| Moving / morphing | `Curves.easeInOut` | cubic-bezier(0.2, 0, 0, 1) | +| Element exiting | `Curves.easeIn` | acceptable ONLY for exits | +| Continuous (spinner) | `Curves.linear` | linear | + +Never use `Curves.easeIn` for entering UI; it delays movement exactly when the user is watching. + +## Durations + +Exit animations should be 20-30% faster than their enter counterpart. + +| Category | Duration | +|---|---| +| Micro (button press, tooltip, hover) | 100-200ms (press feedback 75-100ms) | +| Standard (dropdown, popover, select) | 200-300ms | +| Large (modal, drawer, route transition) | 300-500ms | +| Hard cap for UI feedback | 300ms | + +```dart +// Standard dropdown +AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ... +) + +// Large modal enter +PageRouteBuilder( + transitionDuration: const Duration(milliseconds: 350), + ... +) +``` + +## Micro-interactions in Flutter + +Every interactive element has six states: default, hover (desktop), focused, pressed, disabled, +loading. A missing loading or disabled state is the most common quality gap. + +In Wind `className`, the state prefixes are: + +```dart +WButton( + className: ''' + transition-colors duration-100 + hover:bg-primary-container + focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 + active:opacity-90 + disabled:opacity-50 + motion-safe:active:scale-95 + ''', + ... +) +``` + +- Hover: color/opacity shift, 100-150ms. On mobile there is no hover; `WAnchor` gates hover + behind pointer-device detection automatically. +- Press: `motion-safe:active:scale-95` at ~75ms for tactile feedback. Gate with `motion-safe:` + so it does not fire under reduced motion. +- Focus: `focus-visible:ring-2 focus-visible:ring-primary`; keyboard only (never bare `:focus`); + never removed. +- Disabled: `disabled:opacity-50`. Also set `onPressed: null` to disable the GestureDetector. +- Loading: show a loading spinner or skeleton; do not disable without visual feedback. + +## Overlays and transitions in Flutter + +For modals, bottom sheets, and drawers, Flutter's built-in route system handles the animation +curve. Customize via `PageRouteBuilder` or `showModalBottomSheet` parameters: + +```dart +// Bottom sheet: slide up from bottom, ease-out +showModalBottomSheet( + context: context, + isScrollControlled: true, + transitionAnimationController: AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ), + builder: (_) => BottomSheetContent(), +) +``` + +For in-page expand/collapse (accordion, inline panel): + +```dart +AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + child: isExpanded ? ExpandedContent() : const SizedBox.shrink(), +) +``` + +Pattern: opacity + small directional slide toward the trigger gives spatial context. Keep +tooltip/dropdown entrances 100-150ms. + +## Route transitions + +In magic_example the router is `go_router`. Route transitions use `CustomTransitionPage`: + +```dart +CustomTransitionPage( + child: const DashboardView(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + transitionDuration: const Duration(milliseconds: 250), +) +``` + +Auth routes use `RouteTransition.none` (no animation between auth screens). For content +screens a simple fade (150-250ms, `Curves.easeOut`) is the safest default. + +## Loading and skeleton states + +- Show a `Skeleton` component within ~300ms if data has not arrived. +- Use `Skeleton(shape: SkeletonShape.block)` for card-shaped areas, `SkeletonShape.text` for + text lines, `SkeletonShape.circle` for avatars. +- Reserve spinners for short blocking mutations (submit, auth). +- Gate the shimmer pulse animation behind `motion-safe:`: + +```dart +Skeleton( + className: 'motion-safe:animate-pulse bg-surface-container-high rounded', +) +``` + +## Performance in Flutter + +- Animate ONLY properties handled by the compositor: `opacity`, `transform` (via `Transform` or + `AnimatedContainer`). Avoid animating `width`, `height`, `padding`, or `margin` (layout pass + every frame). +- Use `RepaintBoundary` around complex animated subtrees to isolate repaints. +- Avoid many simultaneous animations in one viewport. Use `ListView.builder` or + `SliverList` for long off-screen lists; Flutter will tree-shake non-visible widgets. +- `TickerProviderStateMixin` properly disposes controllers; always call `controller.dispose()` + in `State.dispose()`. + +## Accessibility and restraint + +Respect the OS "Reduce Motion" setting via `MediaQuery.of(context).disableAnimations`: + +```dart +final reduceMotion = MediaQuery.of(context).disableAnimations; + +// Wind className: motion-safe: prefix gates the animation token +WButton(className: 'motion-safe:active:scale-95 ...') + +// Dart animation controller: skip or instant when reduced +controller.duration = reduceMotion + ? Duration.zero + : const Duration(milliseconds: 200); +``` + +Disable under reduced motion: parallax, scale/zoom, large pan/translate, looping autoplay, +staggered reveals, shimmer. Keep or substitute: opacity fades, color transitions, instant snap. +Substitute, do not just delete, motion that conveys state (for example, replace a spinner with a +static icon when motion is disabled, do not hide loading state entirely). + +No content flashes more than 3 times per second. Auto-playing motion over 5 seconds needs a +pause control. + +## Per-screen motion checklist + +Before marking any screen done: + +1. Every animated element guards against reduced motion via `motion-safe:` or + `MediaQuery.of(context).disableAnimations`. +2. Only `opacity` and `transform` animate; no layout-triggering properties. +3. Entrances `Curves.easeOut`; exits faster; no `Curves.easeIn` on entrances. +4. UI feedback under 200ms; large transitions under 500ms. +5. Six interactive states present (default, hover, focus, active, disabled, loading). +6. Auto-play is controllable; nothing flashes >3 times/sec. + +## See also + +- [DESIGN.md](../DESIGN.md): brand personality and DESIGN.md motion direction +- [accessibility-wcag.md](accessibility-wcag.md): WCAG 2.2.2 / 2.3.1 reduced-motion requirements +- [wind-responsive.md](wind-responsive.md): safe-area, layout transitions +- [apple-hig.md](apple-hig.md): Apple spring-based motion feel +- [material-design-3.md](material-design-3.md): M3 easing tokens + +## Sources + +- Emil Kowalski / animations.dev (easing + duration tables, frequency rule). +- Material Design 3 motion: easing/duration tokens (m3.material.io/styles/motion). +- Apple HIG motion (developer.apple.com); WWDC23 "Animate with springs". +- Flutter documentation: CurvedAnimation, AnimationController, AnimatedContainer, AnimatedSize, + MediaQuery.disableAnimations, RepaintBoundary. +- WCAG 2.2: 2.2.2 / 2.3.1 / 2.3.3. diff --git a/docs/design-culture/refactoring-ui.md b/docs/design-culture/refactoring-ui.md new file mode 100644 index 0000000..2047708 --- /dev/null +++ b/docs/design-culture/refactoring-ui.md @@ -0,0 +1,185 @@ +# Refactoring UI craft (Flutter/Wind reference) + +Read this for every screen, regardless of design language. Apple/Material decide the visual +identity; this decides whether the result looks designed or amateur. These are defaults to apply, +not options to choose. + +Source: "Refactoring UI" (Wathan and Schoger) adapted to Flutter/Wind/Magic idioms. + +## Hierarchy (the highest-leverage skill) + +Not all elements are equal. Give every screen one clear primary action; demote the rest. Build a +pyramid: one primary, a few secondary, the rest tertiary. + +Emphasize by de-emphasizing. To make the primary stand out, soften the secondary; do not just +enlarge the primary. + +Use the levers in this order: color/contrast first, font-weight second, size last. Size alone is +a weak signal. + +- Cap variety: 2-3 font weights and 2-3 text-contrast levels per screen. More tiers read as noise. +- For secondary text use lower contrast, not smaller size. Use `text-fg-muted` rather than + dropping the font size. +- For labels, prefer format and context over a label ("12 left in stock", not "Stock: 12"). +- Style by visual role, not widget type. A heading can be small; a button text can be quiet. + +### Applying hierarchy with Wind tokens + +```dart +// Primary action: high contrast, filled +Button(variant: ButtonVariant.primary, child: Text('Save changes')) + +// Secondary: outlined or ghost +Button(variant: ButtonVariant.secondary, child: Text('Cancel')) + +// Destructive: reserve bg-destructive +Button(variant: ButtonVariant.destructive, child: Text('Delete account')) + +// Primary body text +WText('Your profile has been updated.', className: 'text-fg text-base') + +// Secondary helper text +WText('Changes take effect immediately.', className: 'text-fg-muted text-sm') +``` + +## Anti-AI-slop tells + +The following patterns are how generated or rushed UI gives itself away. Avoid all of them. + +- **Purple-on-gray everything**: primary color leaked onto every surface. Reserve `bg-primary` + for the one primary action per view. Everything else is neutral. +- **All text the same size and weight**: no hierarchy. Use `Typography` variants; vary weight + before varying size. +- **Cards with too much padding and no content**: a card that is mostly whitespace with one short + line of text. Cards need meaningful content density. +- **Every section the same vertical rhythm**: no breathing room variation between a dense form + and a spacious hero. Use the spacing scale deliberately. +- **Placeholder content left in production**: "Lorem ipsum", "User Name", "Coming soon" states + with no real data shape. Design with realistic data from the start. +- **No empty state designed**: lists and feeds without a "no data" state. Every list needs a + designed empty state (icon + title + one clear CTA). +- **Icon-only buttons with no label and no Semantics**: inaccessible and confusing. Add a + `Semantics(label: ...)` wrapper or a visible label. +- **Shadows on everything**: depth without meaning. Reserve `shadow-sm` for cards/inputs, + `shadow-md` for dropdowns, `shadow-lg` for modals. Do not stack. +- **Flat gray on gray**: a form input on a surface that is the same `bg-surface` color. Use + `bg-surface-container-high` for input backgrounds to give the minimal contrast needed. + +## Spacing and layout + +- Start with too much whitespace, then remove. Under-spacing is the more common failure. +- Enforce the relationship rule: space INSIDE a group < space BETWEEN groups < space around a + section. Proximity is how users perceive grouping. +- Use the Wind 4px logical scale only: `p-1`(4px) `p-2`(8px) `p-3`(12px) `p-4`(16px) + `p-6`(24px) `p-8`(32px) `p-12`(48px) `p-16`(64px). If a value is not on the scale, snap to + the nearest step; never invent an arbitrary offset. +- Do not fill available width. Constrain text columns via a `ConstrainedBox` with + `maxWidth: 600` (equivalent to `max-w-prose`). Use `PageContainer` for page shells. See + [wind-responsive.md](wind-responsive.md). +- Dense data UIs (tables, dashboards) use a compressed scale (drop one or two steps), not a + different arbitrary set. + +### Spacing in practice + +```dart +// Section separation: gap-6 (24px) between sections +WDiv( + className: 'flex flex-col gap-6 p-4', + children: [ + SectionHeader(), + FormContent(), // gap between items inside: gap-4 + ActionRow(), + ], +) + +// Group separation: gap-4 (16px) between form fields +WDiv( + className: 'flex flex-col gap-4', + children: [nameField, emailField, passwordField], +) +``` + +## Typography + +- Use `Typography` component variants rather than raw `text-*` sizes. The variants are calibrated + to a modular scale from the DESIGN.md font definitions. +- The DESIGN.md font (Inter by default) is authoritative. Do not add a second typeface per-component. +- Weight: `font-normal` (400) for body, `font-medium` (500) for UI labels, `font-semibold` (600) + for headings and primary actions. Never below 400 for body or small text. +- Line-height is inverse to size: body `leading-relaxed` (1.5-1.6), UI components `leading-tight` + (1.2-1.3), headings `leading-snug` (1.1-1.2). +- Left-align body; never center multi-line prose. Right-align numeric columns. +- Letter-spacing: leave body at the font default. For all-caps labels add `tracking-wide` + (`+0.05em`). For large display headings add `tracking-tight` (`-0.01em`). + +## Color + +- Define the full palette up front in DESIGN.md; never invent a shade at use-time. +- Consume semantic tokens; never hardcode hex inside a component. The 17 token names are defined + in [DESIGN.md](../DESIGN.md). +- Never put neutral `text-fg-muted` on a colored surface. Use the matching `on-*` token instead + (`text-on-primary` on `bg-primary`, `text-on-destructive` on `bg-destructive`). +- Use saturated color sparingly, for the one thing that must stand out. Most hierarchy comes from + contrast, weight, and spacing. +- Never rely on color alone for state: pair with an icon or label. See + [accessibility-wcag.md](accessibility-wcag.md). + +## Depth and finishing polish + +- Light comes from above: if using shadows, they fall downward. Assign shadows by role: + - Cards, inputs: `shadow-sm` + - Dropdowns, tooltips: `shadow-md` + - Modals, popovers: `shadow-lg` +- Design grayscale-first. If the hierarchy reads without color, color reinforces it; if it only + works with color, fix spacing/contrast first. +- Use fewer borders. Separate with a background-contrast shift (`bg-surface` vs + `bg-surface-container`), a subtle shadow, or extra spacing before reaching for + `border-color-border`. +- Add finishing touches: + - Accent borders: a colored left strip on cards or alerts (`border-l-2 border-primary`). + - Designed empty states: icon + title + one clear CTA. Never ship a bare "No results". + - Designed loading states: `Skeleton` component matching the real layout shape. Do not + show a spinner where structure is known. + +### Empty state example + +```dart +EmptyState( + icon: Icons.notifications_none, + title: 'No notifications yet', + description: 'You will see updates here when activity happens.', + action: Button( + variant: ButtonVariant.secondary, + child: Text('Refresh'), + onPressed: controller.refresh, + ), +) +``` + +## How to apply in this codebase + +- Hierarchy: use `Typography` variants + `font-semibold` and `text-fg` vs `text-fg-muted` for + contrast tiers. Reserve `bg-primary` for the single primary action per view; secondary actions + use Button `secondary` or `ghost` variant. +- Spacing: use the 4px Wind scale (`gap-4`/`p-4`/`gap-6`) and keep inside < between < section. + Wrap text columns in a `ConstrainedBox`; use `PageContainer` for page shells. +- Color: consume the 17 semantic tokens from DESIGN.md. On a colored surface use the matching + `on-*` token; never use `text-fg-muted` on a filled background. +- Depth: `rounded-lg` for cards/dialogs, `rounded-md` for buttons. Prefer + `border-color-border` only where a contrast shift will not do. +- Polish: build the mockup with realistic data first. Always design the empty and loading state + of any list or async surface. + +## See also + +- [DESIGN.md](../DESIGN.md): token definitions, typography scale, spacing scale +- [accessibility-wcag.md](accessibility-wcag.md): contrast requirements +- [material-design-3.md](material-design-3.md): M3 role-to-token mapping +- [wind-responsive.md](wind-responsive.md): layout patterns and PageContainer +- [motion-interaction.md](motion-interaction.md): loading states, skeleton, transition patterns + +## Sources + +- "Refactoring UI" by Adam Wathan and Steve Schoger (refactoringui.com). +- Flutter documentation: ConstrainedBox, TextStyle, MediaQuery. +- Wind token documentation: semantic aliases, 4px spacing scale. diff --git a/docs/design-culture/wind-responsive.md b/docs/design-culture/wind-responsive.md new file mode 100644 index 0000000..4957682 --- /dev/null +++ b/docs/design-culture/wind-responsive.md @@ -0,0 +1,263 @@ +# Wind responsive layout (Flutter reference) + +Read this when building any screen layout. Wind's responsive system is breakpoint-prefix-driven, +not CSS media-query-driven. This doc covers breakpoints, page containers, safe-area handling, +navigation layout patterns, hit targets, and mobile-first rules. + +## Breakpoints + +Wind uses three responsive prefixes. They apply to the logical viewport width reported by +`MediaQuery.of(context).size.width`: + +| Prefix | Min width | Typical device | +|---|---|---| +| (none, default) | 0px | All screens, mobile first | +| `sm:` | 480px | Large phones, landscape | +| `md:` | 768px | Tablets, desktop | +| `lg:` | 1024px | Wide desktop | + +Breakpoints are mobile-first: a class without a prefix applies to ALL widths; a prefixed class +overrides from that breakpoint up. + +```dart +// Single-column on mobile, two-column on md+ +WDiv( + className: 'flex flex-col md:flex-row gap-4', + children: [MainContent(), Sidebar()], +) +``` + +Wind has no CSS `@media` query mechanism; the breakpoint resolution runs inside the Wind parser +using the current `MediaQuery` width. Do not try to use CSS media queries or `LayoutBuilder` +directly for layout decisions that are already expressible as Wind breakpoint prefixes. + +## PageContainer + +`PageContainer` (from magic_starter) constrains the content width and applies consistent +horizontal gutters. Always wrap page-level content in `PageContainer`; do not build custom max- +width constraints per-screen. + +```dart +PageContainer( + child: WDiv( + className: 'flex flex-col gap-6 py-6', + children: [...], + ), +) +``` + +`PageContainer` sets `maxWidth: 1200px` and the horizontal padding: + +- Mobile: `px-4` (16px each side) +- `md:` and above: `px-6` (24px each side) + +Do not add extra horizontal padding inside `PageContainer`; let it handle the gutters. + +## Safe area handling + +On iOS and Android, system UI (status bar, home indicator, notch) can overlap content. Always +wrap the root of a full-screen page in `SafeArea`: + +```dart +// In a page-level build method +Scaffold( + body: SafeArea( + child: PageContainer(child: content), + ), +) +``` + +`AppLayout` and `GuestLayout` apply `SafeArea` at the shell level; views rendered inside the +layout shell do NOT need to re-wrap in `SafeArea`. + +For custom bottom actions (a sticky CTA at the bottom of the screen), account for the home +indicator via `MediaQuery.viewPaddingOf(context).bottom`: + +```dart +Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewPaddingOf(context).bottom + 16, + ), + child: ActionButton(), +) +``` + +Dialog safe area uses the modal formula: `safeHeight = screenHeight - top - bottom insets`, +then `maxHeight = safeHeight * 0.85`. The `Dialog` and `BottomSheet` components in magic_starter +handle this automatically via `MediaQuery.viewPaddingOf(context)`. + +## Navigation: sidebar vs bottom nav + +The `AppLayout` shell switches navigation patterns by breakpoint: + +| Breakpoint | Navigation pattern | +|---|---| +| Below `md:` (mobile) | Bottom navigation bar + hamburger drawer | +| `md:` and above | Sidebar navigation rail | + +To implement this in a custom layout follow the `AppLayout` pattern: + +```dart +// Show sidebar on desktop, hide on mobile +WDiv( + className: 'hidden md:flex flex-col w-64 bg-surface-container border-r border-color-border', + children: [SidebarContent()], +) + +// Show bottom nav on mobile, hide on desktop +WDiv( + className: 'flex md:hidden flex-row bg-surface-container border-t border-color-border', + children: [BottomNavItems()], +) +``` + +Never show both a sidebar and a bottom nav at the same breakpoint. On mobile, the sidebar +appears as a drawer (use `Drawer` + `Scaffold.drawer`). + +## 44px hit targets + +Every interactive element must have a minimum 44x44 logical-px hit target (Apple HIG / Material +recommendation; WCAG 2.5.8 floor is 24px). + +In Wind className: `min-h-11` equals 44px logical height. The default Button `md`/`lg` sizes +already clear this. + +For icon-only controls (icon buttons, nav items, close buttons): + +```dart +// Ensure 44px hit target on a small icon +SizedBox( + width: 44, + height: 44, + child: Center( + child: Icon(Icons.close, size: 20), + ), +) +``` + +Or with Wind: `WButton(className: 'min-h-11 min-w-11 p-0 flex items-center justify-center', ...)` + +Bottom nav items must be at least 44px tall. Sidebar nav items should be at least 44px tall with +sufficient horizontal padding (`py-2 px-3` minimum on a nav item gives ~40px; add `min-h-11` to +guarantee it). + +## Mobile-first composition rules + +1. Start with the mobile layout (no prefix). Confirm it reads on a 375px width. +2. Add `md:` overrides for tablet/desktop changes (column -> row, hidden -> shown, etc.). +3. Add `lg:` overrides only when desktop needs a third layout step. +4. Never write `sm:hidden` or `sm:flex` as the primary display rule; start mobile-visible, then + hide at breakpoints. + +```dart +// Correct: visible by default, hidden on desktop +WDiv(className: 'flex md:hidden', children: [MobileMenu()]) + +// Correct: hidden by default (inline), visible on desktop +WDiv(className: 'hidden md:flex', children: [Sidebar()]) +``` + +## Text and content constraints + +Do not fill available width with text. Constrain reading columns: + +```dart +ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: WText(longBodyText, className: 'text-fg body-md'), +) +``` + +Inside `PageContainer`, text naturally hits the container max-width. For individual narrow +columns (auth forms, settings cards) apply an inner `ConstrainedBox(maxWidth: 480)`. + +## WindRecipe with responsive variants + +When a component has a layout-responsive variant, express it in the recipe's `className` caller +rather than as a recipe variant axis. Responsive breakpoints are caller context, not component +internals: + +```dart +// Caller supplies the responsive layout via className +Card( + className: 'w-full md:w-1/2 lg:w-1/3', + child: ..., +) +``` + +The `WindRecipe` base and variant tokens handle visual state (tone, size, intent). Responsive +width is caller responsibility. + +## Platform prefixes + +Wind provides platform prefixes for platform-specific styles: + +| Prefix | Platform | +|---|---| +| `ios:` | iOS | +| `android:` | Android | +| `web:` | Flutter Web | +| `mobile:` | iOS + Android | +| `macos:` | macOS | + +Use these sparingly for platform-specific affordances (for example, `ios:rounded-xl` to apply +a rounder corner on iOS only). Do not use them as a substitute for breakpoint-based layout. + +## Common layout patterns + +### Full-screen auth page (GuestLayout shell) + +```dart +// GuestLayout constrains to 480px max-width, centered, scrollable +// Views inside it use: +WDiv( + className: 'flex flex-col gap-6 px-4 py-8', + children: [ + Logo(), + Card(child: LoginForm()), + SocialDivider(), + SocialButtons(), + ], +) +``` + +### Dashboard with sidebar (AppLayout shell on desktop) + +```dart +// AppLayout provides the sidebar rail on md+; the view sees only the content slot +WDiv( + className: 'flex flex-col gap-6 p-6', + children: [ + PageHeader(title: 'Dashboard'), + StatsGrid(), + RecentActivity(), + ], +) +``` + +### Responsive two-column form + +```dart +WDiv( + className: 'grid grid-cols-1 md:grid-cols-2 gap-4', + children: [ + FormField(label: 'First name', child: Input(...)), + FormField(label: 'Last name', child: Input(...)), + ], +) +``` + +## See also + +- [DESIGN.md](../DESIGN.md): spacing scale (4px logical grid), gutter/section values +- [accessibility-wcag.md](accessibility-wcag.md): 44px hit targets, SafeArea requirements +- [material-design-3.md](material-design-3.md): navigation by breakpoint (M3 guidance) +- [apple-hig.md](apple-hig.md): screen-edge margins, concentric radii +- [refactoring-ui.md](refactoring-ui.md): whitespace, text column constraints + +## Sources + +- magic_starter `AppLayout` and `GuestLayout` source (`lib/src/ui/layouts/`). +- Wind breakpoint prefix documentation (`wind/CLAUDE.md`). +- Flutter documentation: MediaQuery, SafeArea, Scaffold.drawer, ConstrainedBox. +- Apple HIG Layout guidelines; Material 3 navigation components. diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b77a925 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,647 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 883V9SVA54; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 883V9SVA54; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 883V9SVA54; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..f68554b --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Magic Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + magic_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/app/_plugins.g.dart b/lib/app/_plugins.g.dart new file mode 100644 index 0000000..33a34ea --- /dev/null +++ b/lib/app/_plugins.g.dart @@ -0,0 +1,27 @@ +// GENERATED: do not edit by hand. +// Regenerate via: dart run magic:artisan plugins:refresh +// +// Source: .artisan/plugins.json + +import 'package:fluttersdk_artisan/artisan.dart'; +import 'package:fluttersdk_dusk/cli.dart' show FluttersdkDuskArtisanProvider; +import 'package:fluttersdk_telescope/cli.dart' + show FluttersdkTelescopeArtisanProvider; +import 'package:magic/cli.dart' show MagicArtisanProvider; +import 'package:magic_deeplink/cli.dart' show MagicDeeplinkArtisanProvider; +import 'package:magic_notifications/cli.dart' + show MagicNotificationsArtisanProvider; +import 'package:magic_social_auth/cli.dart' show MagicSocialAuthArtisanProvider; +import 'package:magic_starter/cli.dart' show MagicStarterArtisanProvider; + +List autoDiscoveredProviders() { + return [ + FluttersdkDuskArtisanProvider(), + FluttersdkTelescopeArtisanProvider(), + MagicArtisanProvider(), + MagicDeeplinkArtisanProvider(), + MagicNotificationsArtisanProvider(), + MagicSocialAuthArtisanProvider(), + MagicStarterArtisanProvider(), + ]; +} diff --git a/lib/app/commands/_index.g.dart b/lib/app/commands/_index.g.dart new file mode 100644 index 0000000..8da1c7c --- /dev/null +++ b/lib/app/commands/_index.g.dart @@ -0,0 +1,7 @@ +// AUTO-GENERATED — DO NOT EDIT. +// Regenerate via: artisan commands:refresh +// (Also auto-updated by: artisan make:command ) + +import 'package:fluttersdk_artisan/artisan.dart'; + +List get commands => []; diff --git a/lib/app/kernel.dart b/lib/app/kernel.dart new file mode 100644 index 0000000..fe14464 --- /dev/null +++ b/lib/app/kernel.dart @@ -0,0 +1,55 @@ +// Import Magic to access Kernel, middleware base classes, etc.: +import 'package:magic/magic.dart'; +import 'middleware/ensure_authenticated.dart'; +import 'middleware/redirect_if_authenticated.dart'; + +/// The HTTP Kernel. +/// +/// Register all middleware here, similar to Laravel's `app/Http/Kernel.php`. +/// +/// ## Usage +/// +/// This function is called automatically by `RouteServiceProvider.register()`. +/// You do not need to call it manually. +/// +/// ## Global Middleware +/// +/// Global middleware runs on EVERY route: +/// +/// ```dart +/// Kernel.global([ +/// () => LoggingMiddleware(), +/// ]); +/// ``` +/// +/// ## Route Middleware +/// +/// Route middleware are named aliases you use in route definitions: +/// +/// ```dart +/// Kernel.registerAll({ +/// 'auth': () => EnsureAuthenticated(), +/// 'guest': () => RedirectIfAuthenticated(), +/// }); +/// ``` +void registerKernel() { + // --------------------------------------------------------------------------- + // Global Middleware + // --------------------------------------------------------------------------- + // Kernel.global([ + // () => LoggingMiddleware(), + // ]); + + // --------------------------------------------------------------------------- + // Route Middleware + // --------------------------------------------------------------------------- + // Uncomment and add your middleware aliases below: + // Kernel.registerAll({ + // 'auth': () => EnsureAuthenticated(), + // 'guest': () => RedirectIfAuthenticated(), + // }); + Kernel.registerAll({ + 'auth': () => EnsureAuthenticated(), + 'guest': () => RedirectIfAuthenticated(), + }); +} diff --git a/lib/app/middleware/ensure_authenticated.dart b/lib/app/middleware/ensure_authenticated.dart new file mode 100644 index 0000000..cce3e9c --- /dev/null +++ b/lib/app/middleware/ensure_authenticated.dart @@ -0,0 +1,23 @@ +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart'; + +/// Middleware that redirects unauthenticated users to the login page. +/// +/// Use this in auth-only route groups: +/// +/// ```dart +/// MagicRoute.group( +/// middleware: [EnsureAuthenticated()], +/// routes: () { /* protected routes */ }, +/// ); +/// ``` +class EnsureAuthenticated extends MagicMiddleware { + @override + Future handle(void Function() next) async { + if (!Auth.check()) { + MagicRoute.to(MagicStarterConfig.loginRoute()); + return; + } + next(); + } +} diff --git a/lib/app/middleware/redirect_if_authenticated.dart b/lib/app/middleware/redirect_if_authenticated.dart new file mode 100644 index 0000000..29992ae --- /dev/null +++ b/lib/app/middleware/redirect_if_authenticated.dart @@ -0,0 +1,23 @@ +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart'; + +/// Middleware that redirects authenticated users away from guest-only pages. +/// +/// Use this in guest-only route groups (auth pages): +/// +/// ```dart +/// MagicRoute.group( +/// middleware: [RedirectIfAuthenticated()], +/// routes: () { /* auth pages */ }, +/// ); +/// ``` +class RedirectIfAuthenticated extends MagicMiddleware { + @override + Future handle(void Function() next) async { + if (Auth.check()) { + MagicRoute.to(MagicStarterConfig.homeRoute()); + return; + } + next(); + } +} diff --git a/lib/app/models/team.dart b/lib/app/models/team.dart new file mode 100644 index 0000000..ca6fa2d --- /dev/null +++ b/lib/app/models/team.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; + +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart'; + +/// Team model. +/// +/// Represents a team/workspace in the application. Extends the Magic +/// ORM [Model] with [HasTimestamps] and [InteractsWithPersistence] mixins +/// to provide full persistence and timestamp tracking. +/// +/// ## Usage +/// +/// ```dart +/// // Create a new team +/// final team = Team()..name = 'My Team'; +/// await team.save(); +/// +/// // Find a team by ID +/// final team = await Team.find('abc123'); +/// Log.debug(team?.name); +/// ``` +/// +/// ## Plugin Interop +/// +/// ```dart +/// // Convert to MagicStarterTeam for the magic_starter team resolver +/// final magicStarterTeam = team.toMagicStarterTeam(); +/// ``` +class Team extends Model with HasTimestamps, InteractsWithPersistence { + /// The table associated with the model. + @override + String get table => 'teams'; + + /// The API resource for remote operations. + @override + String get resource => 'teams'; + + /// Whether the primary key is auto-incrementing. + /// + /// Set to false because this app uses string UUIDs as primary keys. + @override + bool get incrementing => false; + + /// The attributes that are mass assignable. + @override + List get fillable => ['name']; + + /// The attributes that should be cast. + @override + Map get casts => {}; + + // --------------------------------------------------------------------------- + // Typed Accessors + // --------------------------------------------------------------------------- + + /// Get the team's ID. + @override + String get id => getAttribute('id')?.toString() ?? ''; + + /// Get the team name. + String? get name => getAttribute('name') as String?; + + /// Set the team name. + set name(String? value) => setAttribute('name', value); + + /// Get the URL of the team's profile photo. + String? get profilePhotoUrl => getAttribute('profile_photo_url') as String?; + + /// Set the URL of the team's profile photo. + set profilePhotoUrl(String? value) => + setAttribute('profile_photo_url', value); + + /// Check if this is a personal team (automatically created for the owner). + bool get isPersonalTeam => getAttribute('personal_team') == true; + + /// Get the ID of the team's owner. + String? get ownerId => getAttribute('owner_id')?.toString(); + + /// Current authenticated user's role in this team (from API response). + /// + /// Typical values: `'owner'`, `'admin'`, `'editor'`, `'member'`. + String? get userRole => getAttribute('user_role') as String?; + + // --------------------------------------------------------------------------- + // Permission Helpers + // --------------------------------------------------------------------------- + + /// Whether the current user can manage team members (owner or admin). + bool get canManageMembers => userRole == 'owner' || userRole == 'admin'; + + /// Whether the current user can edit team settings. + /// + /// Restricted to the owner only. Use [canManageMembers] for broader + /// write access that includes admins. + bool get canEdit => isOwner; + + /// Whether the current authenticated user is the owner of this team. + bool get isOwner => Auth.id() == ownerId; + + // --------------------------------------------------------------------------- + // Static Helpers + // --------------------------------------------------------------------------- + + /// Find a team by ID. + /// + /// Returns `null` if no team with the given [id] exists. + /// + /// ```dart + /// final team = await Team.find('abc123'); + /// ``` + static Future find(dynamic id) => + InteractsWithPersistence.findById(id, Team.new); + + /// Get all teams. + /// + /// ```dart + /// final teams = await Team.all(); + /// ``` + static Future> all() => + InteractsWithPersistence.allModels(Team.new); + + // --------------------------------------------------------------------------- + // Factory Methods + // --------------------------------------------------------------------------- + + /// Create a [Team] from a [Map]. + /// + /// Uses [setRawAttributes] to hydrate the model directly from raw API data, + /// bypassing mass-assignment protection. The [exists] flag is set based on + /// whether the map contains an `id` key. + /// + /// ```dart + /// final team = Team.fromMap({'id': 'abc123', 'name': 'Acme Corp'}); + /// ``` + static Team fromMap(Map map) { + return Team() + ..setRawAttributes(map, sync: true) + ..exists = map.containsKey('id'); + } + + /// Create a [Team] from a JSON string. + /// + /// Decodes [json] and delegates to [fromMap]. + /// + /// ```dart + /// final team = Team.fromJson('{"id":"abc123","name":"Acme Corp"}'); + /// ``` + static Team fromJson(String json) { + final map = jsonDecode(json) as Map; + return Team.fromMap(map); + } + + // --------------------------------------------------------------------------- + // Plugin Interop + // --------------------------------------------------------------------------- + + /// Convert to [MagicStarterTeam] for the magic_starter plugin's team resolver. + /// + /// Used by [MagicStarter.useTeamResolver] so the plugin can access + /// team data without depending on the host app's concrete [Team] class. + /// + /// ```dart + /// MagicStarter.useTeamResolver( + /// currentTeam: () async => (await Team.find(currentId))?.toMagicStarterTeam(), + /// allTeams: () async => (await Team.all()).map((t) => t.toMagicStarterTeam()).toList(), + /// ); + /// ``` + MagicStarterTeam toMagicStarterTeam() { + return MagicStarterTeam( + id: id, + name: name ?? '', + photoUrl: profilePhotoUrl, + isPersonalTeam: isPersonalTeam, + ); + } +} diff --git a/lib/app/models/user.dart b/lib/app/models/user.dart new file mode 100644 index 0000000..33abecc --- /dev/null +++ b/lib/app/models/user.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:magic/magic.dart'; + +import 'team.dart'; + +/// User model. +/// +/// Represents the authenticated user in the application. Extends the Magic +/// ORM [Model] with [HasTimestamps], [InteractsWithPersistence], and +/// [Authenticatable] mixins to provide full persistence, timestamp tracking, +/// and authentication support. +/// +/// ## Usage with Typed Accessors +/// +/// ```dart +/// final user = await User.find('abc123'); +/// Log.debug(user?.name); // Uses typed accessor +/// user?.name = 'Updated Name'; +/// await user?.save(); +/// ``` +/// +/// ## Usage with Convenient get/set +/// +/// ```dart +/// final user = await User.find('abc123'); +/// Log.debug(user?.get('name', defaultValue: 'Unknown')); +/// user?.set('name', 'Updated Name'); +/// await user?.save(); +/// ``` +/// +/// ## Accessing the Authenticated User +/// +/// ```dart +/// final user = User.current; +/// Log.debug(user.name); +/// ``` +class User extends Model + with HasTimestamps, InteractsWithPersistence, Authenticatable { + /// The table associated with the model. + @override + String get table => 'users'; + + /// The API resource for remote operations. + @override + String get resource => 'users'; + + /// Whether the primary key is auto-incrementing. + /// + /// Set to false because this app uses string UUIDs as primary keys. + @override + bool get incrementing => false; + + /// The attributes that are mass assignable. + @override + List get fillable => [ + 'name', + 'email', + 'phone', + 'timezone', + 'language', + ]; + + /// The attributes that should be cast. + @override + Map get casts => {}; + + // --------------------------------------------------------------------------- + // Typed Accessors + // --------------------------------------------------------------------------- + + /// Get the user's ID. + @override + String get id => getAttribute('id')?.toString() ?? ''; + + /// Get the user's name. + String? get name => getAttribute('name') as String?; + + /// Set the user's name. + set name(String? value) => setAttribute('name', value); + + /// Get the user's email address. + String? get email => getAttribute('email') as String?; + + /// Set the user's email address. + set email(String? value) => setAttribute('email', value); + + /// Get the user's phone number. + String? get phone => getAttribute('phone') as String?; + + /// Set the user's phone number. + set phone(String? value) => setAttribute('phone', value); + + /// Get the user's timezone identifier (e.g. `America/New_York`). + String? get timezone => getAttribute('timezone') as String?; + + /// Set the user's timezone identifier. + set timezone(String? value) => setAttribute('timezone', value); + + /// Get the user's preferred language code (e.g. `en`, `tr`). + String? get language => getAttribute('language') as String?; + + /// Set the user's preferred language code. + set language(String? value) => setAttribute('language', value); + + /// Get the URL of the user's profile photo. + String? get profilePhotoUrl => getAttribute('profile_photo_url') as String?; + + /// The user's current team. + Team? get currentTeam { + final Map? data = + getAttribute('current_team') as Map?; + return data != null ? Team.fromMap(data) : null; + } + + /// All teams the user belongs to. + List get allTeams { + final List data = + getAttribute('all_teams') as List? ?? []; + return data.map((t) => Team.fromMap(t as Map)).toList(); + } + + // --------------------------------------------------------------------------- + // Static Helpers + // --------------------------------------------------------------------------- + + /// Find a user by ID. + /// + /// Returns `null` if no user with the given [id] exists. + /// + /// ```dart + /// final user = await User.find('abc123'); + /// ``` + static Future find(dynamic id) => + InteractsWithPersistence.findById(id, User.new); + + /// Get all users. + /// + /// ```dart + /// final users = await User.all(); + /// ``` + static Future> all() => + InteractsWithPersistence.allModels(User.new); + + /// Get the currently authenticated user. + /// + /// Returns an empty [User] instance when no user is authenticated. Callers + /// should prefer `Auth.check()` before accessing user data to avoid + /// operating on an empty model. + /// + /// ```dart + /// if (Auth.check()) { + /// final user = User.current; + /// Log.debug(user.name); + /// } + /// ``` + static User get current => Auth.user() ?? User(); + + // --------------------------------------------------------------------------- + // Flutter-Familiar Factory Methods + // --------------------------------------------------------------------------- + + /// Create a [User] from a [Map]. + /// + /// Uses [setRawAttributes] to hydrate the model directly from raw API data, + /// bypassing mass-assignment protection. The [exists] flag is set based on + /// whether the map contains an `id` key. + /// + /// ```dart + /// final user = User.fromMap({'id': 'abc123', 'name': 'Jane', 'email': 'jane@example.com'}); + /// ``` + static User fromMap(Map map) { + return User() + ..setRawAttributes(map, sync: true) + ..exists = map.containsKey('id'); + } + + /// Create a [User] from a JSON string. + /// + /// Decodes [json] and delegates to [fromMap]. + /// + /// ```dart + /// final user = User.fromJson('{"id":"abc123","name":"Jane"}'); + /// ``` + static User fromJson(String json) { + final map = jsonDecode(json) as Map; + return User.fromMap(map); + } +} diff --git a/lib/app/providers/app_service_provider.dart b/lib/app/providers/app_service_provider.dart new file mode 100644 index 0000000..3d2e6fc --- /dev/null +++ b/lib/app/providers/app_service_provider.dart @@ -0,0 +1,76 @@ +import 'package:magic/magic.dart'; +import 'package:flutter/material.dart'; +import 'package:magic_starter/magic_starter.dart'; +import '../models/user.dart'; + +/// Application Service Provider. +/// +/// Use this provider to bind your own services to the IoC container and +/// to perform any bootstrap logic that requires other services to be ready. +class AppServiceProvider extends ServiceProvider { + AppServiceProvider(super.app); + + @override + void register() { + // Bind your services here (sync only — do not resolve other services). + // Example: + // app.singleton('my_service', () => MyService()); + } + + @override + Future boot() async { + // Perform async bootstrap logic here. + // + // IMPORTANT: Call setUserFactory() so Auth.user() returns your model: + // Auth.manager.setUserFactory((data) => User.fromMap(data)); + // Magic Starter: Register user factory for auth session restoration. + Auth.manager.setUserFactory((data) => User.fromMap(data)); + MagicStarter.useUserModel((data) => User.fromMap(data)); + + // Magic Starter: Navigation items for sidebar and mobile bottom bar. + MagicStarter.useNavigation( + mainItems: [ + MagicStarterNavItem( + icon: Icons.dashboard_outlined, + labelKey: 'nav.dashboard', + path: MagicStarterConfig.homeRoute(), + ), + MagicStarterNavItem( + icon: Icons.settings_outlined, + labelKey: 'nav.settings', + path: MagicStarterConfig.profileRoute(), + ), + ], + bottomItems: [ + MagicStarterNavItem( + icon: Icons.dashboard_outlined, + labelKey: 'nav.dashboard', + path: MagicStarterConfig.homeRoute(), + ), + MagicStarterNavItem( + icon: Icons.settings_outlined, + labelKey: 'nav.settings', + path: MagicStarterConfig.profileRoute(), + ), + ], + ); + + // Magic Starter: Logout callback. + MagicStarter.useLogout(() async { + await Auth.logout(); + MagicRoute.to(MagicStarterConfig.loginRoute()); + }); + + // Magic Starter: Supported locale options for profile settings. + MagicStarter.useLocaleOptions({'en': 'English'}); + + // Magic Starter: Team resolver for sidebar team switcher. + MagicStarter.useTeamResolver( + currentTeam: () => User.current.currentTeam?.toMagicStarterTeam(), + allTeams: () => + User.current.allTeams.map((t) => t.toMagicStarterTeam()).toList(), + onSwitch: (teamId) => + MagicStarterTeamController.instance.switchTeam(teamId), + ); + } +} diff --git a/lib/app/providers/route_service_provider.dart b/lib/app/providers/route_service_provider.dart new file mode 100644 index 0000000..3a264e7 --- /dev/null +++ b/lib/app/providers/route_service_provider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:magic/magic.dart'; +import 'package:magic_devtools/preview.dart'; + +import '../kernel.dart'; +import '../../routes/app.dart'; +import '../../preview/_previews.g.dart'; +import 'package:magic_starter/magic_starter.dart'; +import 'package:magic_starter/previews.dart' as starter_previews; + +/// Route Service Provider. +/// +/// Registers the HTTP kernel and application routes. +class RouteServiceProvider extends ServiceProvider { + RouteServiceProvider(super.app); + + @override + void register() { + // Register middleware kernel — runs synchronously during bootstrap. + registerKernel(); + } + + @override + Future boot() async { + // Register application route definitions. + registerMagicStarterAuthRoutes(); + registerMagicStarterProfileRoutes(); + registerMagicStarterTeamRoutes(); + registerAppRoutes(); + + // Dev-only component preview catalog. Registered here, in boot(), so it + // lands BEFORE MagicRouter first builds its routerConfig (the router locks + // its route table on first access). The kDebugMode guard lets the optimizer + // prove the whole catalog dead in release; registerRoutes() folds itself + // out behind kReleaseMode + PREVIEW_ENABLED as a second line of defence. + if (kDebugMode) { + // App-owned previews (foundations, feature screens, nav shells) plus the + // full magic_starter component set, surfaced through its dev-only + // `previews.dart` barrel (no duplication). Sorted by label for an + // idea-design-style alphabetical catalog. The whole block is kDebugMode- + // gated, so the starter preview set is tree-shaken from release. + final List entries = [ + ...previewEntries(), + for (final p in starter_previews.starterComponentPreviews()) + PreviewEntry(label: p.label, slug: p.slug, builder: p.builder), + ]..sort((a, b) => a.label.compareTo(b.label)); + MagicPreview.register(entries); + MagicPreview.registerRoutes(); + } + } +} diff --git a/lib/config/app.dart b/lib/config/app.dart new file mode 100644 index 0000000..1d8d9ca --- /dev/null +++ b/lib/config/app.dart @@ -0,0 +1,33 @@ +import 'package:magic/magic.dart'; +import '../app/providers/app_service_provider.dart'; +import '../app/providers/route_service_provider.dart'; +import 'package:magic_deeplink/magic_deeplink.dart'; +import 'package:magic_notifications/magic_notifications.dart'; +import 'package:magic_social_auth/magic_social_auth.dart'; +import 'package:magic_starter/magic_starter.dart'; + +/// Application Configuration. +Map get appConfig => { + 'app': { + 'name': env('APP_NAME', 'My App'), + 'env': env('APP_ENV', 'production'), + 'debug': env('APP_DEBUG', false), + 'key': env('APP_KEY'), + 'providers': [ + (app) => RouteServiceProvider(app), + (app) => CacheServiceProvider(app), + (app) => DatabaseServiceProvider(app), + (app) => LaunchServiceProvider(app), + (app) => LocalizationServiceProvider(app), + (app) => NetworkServiceProvider(app), + (app) => VaultServiceProvider(app), + (app) => BroadcastServiceProvider(app), + (app) => AppServiceProvider(app), + (app) => AuthServiceProvider(app), + (app) => DeeplinkServiceProvider(app), + (app) => NotificationServiceProvider(app), + (app) => SocialAuthServiceProvider(app), + (app) => MagicStarterServiceProvider(app), + ], + }, +}; diff --git a/lib/config/auth.dart b/lib/config/auth.dart new file mode 100644 index 0000000..80e6765 --- /dev/null +++ b/lib/config/auth.dart @@ -0,0 +1,56 @@ +/// Authentication Configuration. +/// +/// ## Guards +/// +/// - `bearer` / `sanctum` — Bearer token (default) +/// - `basic` — HTTP Basic auth +/// - `api_key` — API key auth +/// +/// ## Features +/// +/// - User caching (instant restore) +/// - Auto token refresh on 401 +/// - Driver-agnostic interceptors +final Map authConfig = { + 'auth': { + // ------------------------------------------------------------------------- + // Defaults + // ------------------------------------------------------------------------- + 'defaults': {'guard': 'api'}, + + // ------------------------------------------------------------------------- + // Guards + // ------------------------------------------------------------------------- + 'guards': { + 'api': {'driver': 'bearer'}, + }, + + // ------------------------------------------------------------------------- + // Endpoints + // ------------------------------------------------------------------------- + 'endpoints': { + 'user': '/auth/user', // Fetch user on restore + 'refresh': '/auth/refresh', // Refresh access token + }, + + // ------------------------------------------------------------------------- + // Token + // ------------------------------------------------------------------------- + 'token': { + 'key': 'auth_token', + 'refresh_key': 'refresh_token', + 'header': 'Authorization', + 'prefix': 'Bearer', + }, + + // ------------------------------------------------------------------------- + // Cache + // ------------------------------------------------------------------------- + 'cache': {'user_key': 'auth_user'}, + + // ------------------------------------------------------------------------- + // Auto Restore + // ------------------------------------------------------------------------- + 'auto_refresh': true, + }, +}; diff --git a/lib/config/broadcasting.dart b/lib/config/broadcasting.dart new file mode 100644 index 0000000..776c275 --- /dev/null +++ b/lib/config/broadcasting.dart @@ -0,0 +1,26 @@ +import 'package:magic/magic.dart'; + +/// Broadcasting configuration. +/// +/// Defines the default broadcasting connection and available connections. +/// See: https://magic.fluttersdk.com/docs/broadcasting +Map get broadcastingConfig => { + 'broadcasting': { + 'default': env('BROADCAST_CONNECTION', 'null'), + 'connections': { + 'reverb': { + 'driver': 'reverb', + 'host': env('REVERB_HOST', 'localhost'), + 'port': int.tryParse(env('REVERB_PORT', '8080')) ?? 8080, + 'scheme': env('REVERB_SCHEME', 'ws'), + 'app_key': env('REVERB_APP_KEY', ''), + 'auth_endpoint': '/broadcasting/auth', + 'reconnect': true, + 'max_reconnect_delay': 30000, + 'activity_timeout': 120, + 'dedup_buffer_size': 100, + }, + 'null': {'driver': 'null'}, + }, + }, +}; diff --git a/lib/config/cache.dart b/lib/config/cache.dart new file mode 100644 index 0000000..0ad53fa --- /dev/null +++ b/lib/config/cache.dart @@ -0,0 +1,9 @@ +import 'package:magic/magic.dart'; + +/// Cache Configuration. +/// +/// - `driver`: `FileStore()` for persistent disk caching. +/// - `ttl`: default time-to-live in seconds. +Map get cacheConfig => { + 'cache': {'driver': FileStore(), 'ttl': 3600}, +}; diff --git a/lib/config/database.dart b/lib/config/database.dart new file mode 100644 index 0000000..d11f1a0 --- /dev/null +++ b/lib/config/database.dart @@ -0,0 +1,12 @@ +/// Database Configuration. +/// +/// Uses SQLite by default. On mobile, files are stored in the app's documents +/// directory. On web, in-memory SQLite is used automatically. +Map get databaseConfig => { + 'database': { + 'default': 'sqlite', + 'connections': { + 'sqlite': {'driver': 'sqlite', 'database': 'database.sqlite'}, + }, + }, +}; diff --git a/lib/config/deeplink.dart b/lib/config/deeplink.dart new file mode 100644 index 0000000..4814721 --- /dev/null +++ b/lib/config/deeplink.dart @@ -0,0 +1,17 @@ +Map get deeplinkConfig => { + 'deeplink': { + 'enabled': true, + 'driver': 'app_links', + 'domain': 'example.com', + 'scheme': 'https', + + 'ios': {'team_id': 'YOUR_TEAM_ID', 'bundle_id': 'com.example.app'}, + + 'android': { + 'package_name': 'com.example.app', + 'sha256_fingerprints': ['YOUR_SHA256_FINGERPRINT'], + }, + + 'paths': ['/*'], + }, +}; diff --git a/lib/config/logging.dart b/lib/config/logging.dart new file mode 100644 index 0000000..28dcf03 --- /dev/null +++ b/lib/config/logging.dart @@ -0,0 +1,16 @@ +/// Logging Configuration. +/// +/// This config file is OPTIONAL. Only create it if you want to customize +/// the logging behaviour. The default channel is `stack` which logs to console. +Map get loggingConfig => { + 'logging': { + 'default': 'stack', + 'channels': { + 'stack': { + 'driver': 'stack', + 'channels': ['console'], + }, + 'console': {'driver': 'console', 'level': 'debug'}, + }, + }, +}; diff --git a/lib/config/magic_starter.dart b/lib/config/magic_starter.dart new file mode 100644 index 0000000..9bad64f --- /dev/null +++ b/lib/config/magic_starter.dart @@ -0,0 +1,36 @@ +// Magic Starter Configuration. +// +// This file is auto-generated by the magic_starter install command. +// Customize feature toggles and route prefixes as needed. + +Map get magicStarterConfig => { + 'magic_starter': { + 'features': { + 'teams': true, + 'registration': true, + 'extended_profile': false, + 'profile_photos': false, + 'social_login': false, + 'two_factor': true, + 'sessions': true, + 'phone_otp': false, + 'newsletter': false, + 'notifications': false, + 'email_verification': true, + 'guest_auth': false, + 'timezones': false, + }, + 'auth': {'email': true, 'phone': false}, + 'defaults': {'locale': 'en', 'timezone': 'UTC'}, + 'supported_locales': ['en', 'tr'], + 'routes': { + 'home': '/', + 'login': '/auth/login', + 'auth_prefix': '/auth', + 'teams_prefix': '/teams', + 'profile_prefix': '/settings', + 'notifications_prefix': '/notifications', + }, + 'legal': {'terms_url': null, 'privacy_url': null}, + }, +}; diff --git a/lib/config/network.dart b/lib/config/network.dart new file mode 100644 index 0000000..0d1db09 --- /dev/null +++ b/lib/config/network.dart @@ -0,0 +1,22 @@ +import 'package:magic/magic.dart'; + +/// Network Configuration. +/// +/// This config file is OPTIONAL. Only create it if you want to use the +/// Magic Network (Http) system. Don't forget to add `NetworkServiceProvider` +/// to your `app.providers` list. +Map get networkConfig => { + 'network': { + 'default': 'api', + 'drivers': { + 'api': { + 'base_url': env('API_URL', 'http://localhost:8000/api/v1'), + 'timeout': 10000, + 'headers': { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + }, + }, + }, +}; diff --git a/lib/config/routing.dart b/lib/config/routing.dart new file mode 100644 index 0000000..e19d3e1 --- /dev/null +++ b/lib/config/routing.dart @@ -0,0 +1,11 @@ +/// Routing Configuration. +/// +/// Controls URL strategy and other routing behavior. +/// Set `'url_strategy'` to `'path'` for clean web URLs (/dashboard instead of /#/dashboard). +/// See: https://magic.fluttersdk.com/docs/basics/routing#url-strategy +Map get routingConfig => { + 'routing': { + 'url_strategy': + null, // 'path' for clean URLs on web, null for default hash strategy + }, +}; diff --git a/lib/config/view.dart b/lib/config/view.dart new file mode 100644 index 0000000..fa8abdd --- /dev/null +++ b/lib/config/view.dart @@ -0,0 +1,21 @@ +/// View Configuration. +/// +/// Customizes the appearance of Magic UI components (dialogs, confirms, +/// loading). These className values are read by MagicFeedback via +/// `Config.get('view.*')`. +Map get viewConfig => { + 'view': { + 'dialog': { + 'class': 'bg-white dark:bg-gray-800 rounded-xl p-6 shadow-2xl max-w-lg', + }, + 'confirm': { + 'container_class': + 'bg-white dark:bg-gray-800 rounded-xl p-6 shadow-2xl w-80', + 'title_class': 'text-lg font-bold text-gray-900 dark:text-white', + 'message_class': 'text-gray-600 dark:text-gray-400 mt-2', + 'button_cancel_class': 'px-4 py-2 text-gray-600 dark:text-gray-300', + 'button_confirm_class': 'px-4 py-2 bg-primary text-white rounded-lg', + 'button_danger_class': 'px-4 py-2 bg-red-500 text-white rounded-lg', + }, + }, +}; diff --git a/lib/config/wind_theme.g.dart b/lib/config/wind_theme.g.dart new file mode 100644 index 0000000..b186b46 --- /dev/null +++ b/lib/config/wind_theme.g.dart @@ -0,0 +1,49 @@ +// GENERATED: do not edit by hand. +// Regenerate via: dart run magic:artisan design:sync +// +// Source of truth: DESIGN.md + +import 'package:flutter/material.dart'; + +/// Semantic wind alias map generated from DESIGN.md. +/// +/// Drop-in for `WindThemeData(aliases: designAliases)`; the +/// keys match the magic_starter token contract. +const Map designAliases = { + 'bg-surface': 'bg-[#FFFFFF] dark:bg-[#030712]', + 'bg-surface-container': 'bg-[#F9FAFB] dark:bg-[#111827]', + 'bg-surface-container-high': 'bg-[#F3F4F6] dark:bg-[#1F2937]', + 'text-fg': 'text-[#111827] dark:text-[#F9FAFB]', + 'text-fg-muted': 'text-[#6B7280] dark:text-[#9CA3AF]', + 'text-fg-disabled': 'text-[#D1D5DB] dark:text-[#4B5563]', + 'bg-primary': 'bg-[#7C3AED] dark:bg-[#8B5CF6]', + 'text-on-primary': 'text-[#FFFFFF] dark:text-[#FFFFFF]', + 'bg-primary-container': 'bg-[#EDE9FE] dark:bg-[#4C1D95]', + 'bg-accent': 'bg-[#4F46E5] dark:bg-[#6366F1]', + 'border-color-border': 'border-[#E5E7EB] dark:border-[#374151]', + 'border-color-border-subtle': 'border-[#F3F4F6] dark:border-[#1F2937]', + 'bg-destructive': 'bg-[#DC2626] dark:bg-[#EF4444]', + 'text-on-destructive': 'text-[#FFFFFF] dark:text-[#FFFFFF]', + 'bg-destructive-container': 'bg-[#FEE2E2] dark:bg-[#7F1D1D]', + 'bg-success': 'bg-[#15803D] dark:bg-[#16A34A]', + 'bg-warning': 'bg-[#D97706] dark:bg-[#B45309]', +}; + +/// The brand `primary` color with a generated 50-900 ramp. +/// +/// Seeded from the DESIGN.md `primary` light hex; consumed by +/// `WindThemeData.toThemeData()` Material interop. +final Map designColors = { + 'primary': MaterialColor(0xFF7C3AED, { + 50: Color(0xFFF5EFFE), + 100: Color(0xFFEADFFC), + 200: Color(0xFFD2BCF9), + 300: Color(0xFFBB99F6), + 400: Color(0xFF9E6DF2), + 500: Color(0xFF7C3AED), + 600: Color(0xFF6D33D1), + 700: Color(0xFF5E2CB4), + 800: Color(0xFF4F2598), + 900: Color(0xFF401E7B), + }), +}; diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..6d248e2 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:magic/magic.dart'; +import 'config/app.dart'; +import 'config/routing.dart'; +import 'config/view.dart'; +import 'config/auth.dart'; +import 'config/database.dart'; +import 'config/network.dart'; +import 'config/cache.dart'; +import 'config/logging.dart'; +import 'config/broadcasting.dart'; +import 'config/deeplink.dart'; +import 'config/wind_theme.g.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:fluttersdk_dusk/dusk.dart'; +import 'package:magic_devtools/dusk.dart'; +import 'package:fluttersdk_telescope/telescope.dart'; +import 'package:magic_devtools/telescope.dart'; +import 'config/magic_starter.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + if (kDebugMode) { + DuskPlugin.install(); + } + if (kDebugMode) { + TelescopePlugin.install(); + TelescopePlugin.registerWatcher(ExceptionWatcher()); + TelescopePlugin.registerWatcher(DumpWatcher()); + } + await Magic.init( + configFactories: [ + () => appConfig, + () => routingConfig, + () => viewConfig, + () => authConfig, + () => databaseConfig, + () => networkConfig, + () => cacheConfig, + () => loggingConfig, + () => broadcastingConfig, + () => deeplinkConfig, + () => magicStarterConfig, + ], + ); + if (kDebugMode) { + MagicTelescopeIntegration.install(); + } + if (kDebugMode) { + MagicDuskIntegration.install(); + } + // Theme generated from DESIGN.md via `design:sync`. Regenerate with: + // dart run magic_example:artisan design:sync + final windTheme = WindThemeData(colors: designColors, aliases: designAliases); + + runApp(MagicApplication(title: 'Magic Example', windTheme: windTheme)); +} diff --git a/lib/preview/_previews.g.dart b/lib/preview/_previews.g.dart new file mode 100644 index 0000000..f5e9730 --- /dev/null +++ b/lib/preview/_previews.g.dart @@ -0,0 +1,65 @@ +// GENERATED: do not edit by hand. +// Regenerate via: dart run magic:artisan previews:refresh +// +// Source: *.preview.dart files discovered under the scan dir. + +import 'package:magic_devtools/preview.dart'; +import 'bottom_menu.preview.dart'; +import 'dashboard_screen.preview.dart'; +import 'foundations.preview.dart'; +import 'login_screen.preview.dart'; +import 'profile_screen.preview.dart'; +import 'register_screen.preview.dart'; +import 'settings_screen.preview.dart'; +import 'sidebar_menu.preview.dart'; +import 'teams_screen.preview.dart'; + +List previewEntries() { + return [ + PreviewEntry( + label: 'BottomMenu', + slug: 'bottom_menu', + builder: (_) => const BottomMenuPreview(), + ), + PreviewEntry( + label: 'DashboardScreen', + slug: 'dashboard_screen', + builder: (_) => const DashboardScreenPreview(), + ), + PreviewEntry( + label: 'Foundations', + slug: 'foundations', + builder: (_) => const FoundationsPreview(), + ), + PreviewEntry( + label: 'LoginScreen', + slug: 'login_screen', + builder: (_) => const LoginScreenPreview(), + ), + PreviewEntry( + label: 'ProfileScreen', + slug: 'profile_screen', + builder: (_) => const ProfileScreenPreview(), + ), + PreviewEntry( + label: 'RegisterScreen', + slug: 'register_screen', + builder: (_) => const RegisterScreenPreview(), + ), + PreviewEntry( + label: 'SettingsScreen', + slug: 'settings_screen', + builder: (_) => const SettingsScreenPreview(), + ), + PreviewEntry( + label: 'SidebarMenu', + slug: 'sidebar_menu', + builder: (_) => const SidebarMenuPreview(), + ), + PreviewEntry( + label: 'TeamsScreen', + slug: 'teams_screen', + builder: (_) => const TeamsScreenPreview(), + ), + ]; +} diff --git a/lib/preview/bottom_menu.preview.dart b/lib/preview/bottom_menu.preview.dart new file mode 100644 index 0000000..a1f624b --- /dev/null +++ b/lib/preview/bottom_menu.preview.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic/magic.dart' show WDiv, WText; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Navigation preview: the authenticated app shell on its MOBILE layout, the +/// bottom navigation bar + drawer, rendered in a phone-width frame. The body is +/// an empty placeholder card so the preview focuses on the navigation chrome, +/// not a crammed screen. +class BottomMenuPreview extends StatelessWidget { + /// Creates the bottom-menu navigation preview. + const BottomMenuPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + chrome: PreviewChrome.appMobile, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => const WDiv( + className: 'p-6', + child: WDiv( + className: ''' + w-full h-80 rounded-xl + border border-color-border bg-surface-container + flex items-center justify-center + ''', + child: WText('Page content', className: 'text-fg-muted text-sm'), + ), + ); +} diff --git a/lib/preview/dashboard_screen.preview.dart b/lib/preview/dashboard_screen.preview.dart new file mode 100644 index 0000000..774dd54 --- /dev/null +++ b/lib/preview/dashboard_screen.preview.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; + +import '../resources/views/dashboard_view.dart'; +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Feature-screen preview: the app dashboard landing view, authenticated and +/// backend-free. +class DashboardScreenPreview extends StatelessWidget { + /// Creates the dashboard screen preview. + const DashboardScreenPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => const DashboardView(); +} diff --git a/lib/preview/foundations.preview.dart b/lib/preview/foundations.preview.dart new file mode 100644 index 0000000..0c1b49a --- /dev/null +++ b/lib/preview/foundations.preview.dart @@ -0,0 +1,137 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart' + show Typography, TypographyVariant; + +/// Foundations preview: the design-token vocabulary the whole system is built +/// on, rendered as live swatches so the catalog shows colors and type next to +/// the components that consume them. +/// +/// Every swatch is painted purely through the 17 semantic alias keys (no raw +/// hex), so it doubles as a visual assertion that the generated theme resolves +/// each role in both light and dark. +class FoundationsPreview extends StatelessWidget { + /// Creates the foundations preview. + const FoundationsPreview({super.key}); + + /// The semantic surface/background roles and their token classNames. + static const List<(String, String)> _surfaceTokens = <(String, String)>[ + ('surface', 'bg-surface'), + ('surface-container', 'bg-surface-container'), + ('surface-container-high', 'bg-surface-container-high'), + ('primary', 'bg-primary'), + ('primary-container', 'bg-primary-container'), + ('accent', 'bg-accent'), + ('destructive', 'bg-destructive'), + ('destructive-container', 'bg-destructive-container'), + ('success', 'bg-success'), + ('warning', 'bg-warning'), + ]; + + /// The foreground/text roles and their token classNames. + static const List<(String, String)> _foregroundTokens = <(String, String)>[ + ('fg', 'text-fg'), + ('fg-muted', 'text-fg-muted'), + ('fg-disabled', 'text-fg-disabled'), + ('on-primary', 'text-on-primary'), + ('on-destructive', 'text-on-destructive'), + ]; + + /// The border roles and their token classNames. + static const List<(String, String)> _borderTokens = <(String, String)>[ + ('border', 'border-color-border'), + ('border-subtle', 'border-color-border-subtle'), + ]; + + @override + Widget build(BuildContext context) { + return WDiv( + className: 'flex flex-col gap-8', + children: [ + _section('Colors: surfaces', _buildSurfaceSwatches()), + _section('Colors: foreground', _buildForegroundSwatches()), + _section('Colors: borders', _buildBorderSwatches()), + _section('Typography', _buildTypeScale()), + ], + ); + } + + /// A labelled section wrapper. + Widget _section(String title, Widget body) { + return WDiv( + className: 'flex flex-col gap-3', + children: [ + WText(title, className: 'text-fg text-sm font-semibold uppercase'), + body, + ], + ); + } + + /// Surface swatches: a filled tile per background role. + Widget _buildSurfaceSwatches() { + return WDiv( + className: 'wrap gap-3', + children: [ + for (final (String name, String token) in _surfaceTokens) + WDiv( + className: 'flex flex-col gap-1 w-36', + children: [ + WDiv( + className: '$token h-16 rounded-lg border border-color-border', + ), + WText(name, className: 'text-fg-muted text-xs'), + ], + ), + ], + ); + } + + /// Foreground swatches: each role's text painted on its natural backdrop. + Widget _buildForegroundSwatches() { + return WDiv( + className: 'wrap gap-3', + children: [ + for (final (String name, String token) in _foregroundTokens) + WDiv( + className: token.startsWith('text-on-') + ? (token == 'text-on-primary' + ? 'bg-primary px-4 py-3 rounded-lg w-44' + : 'bg-destructive px-4 py-3 rounded-lg w-44') + : 'bg-surface-container px-4 py-3 rounded-lg w-44', + child: WText(name, className: '$token text-sm font-medium'), + ), + ], + ); + } + + /// Border swatches: each border role around a neutral tile. + Widget _buildBorderSwatches() { + return WDiv( + className: 'wrap gap-3', + children: [ + for (final (String name, String token) in _borderTokens) + WDiv( + className: 'flex flex-col gap-1 w-36', + children: [ + WDiv(className: 'bg-surface h-16 rounded-lg border-2 $token'), + WText(name, className: 'text-fg-muted text-xs'), + ], + ), + ], + ); + } + + /// Typography scale rendered with the Typography component. + Widget _buildTypeScale() { + return const WDiv( + className: 'flex flex-col gap-2', + children: [ + Typography('Heading 1', variant: TypographyVariant.h1), + Typography('Heading 2', variant: TypographyVariant.h2), + Typography('Heading 3', variant: TypographyVariant.h3), + Typography('Body text renders the default paragraph style.'), + Typography('Caption text', variant: TypographyVariant.caption), + ], + ); + } +} diff --git a/lib/preview/login_screen.preview.dart b/lib/preview/login_screen.preview.dart new file mode 100644 index 0000000..03c0772 --- /dev/null +++ b/lib/preview/login_screen.preview.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic_starter/magic_starter.dart' show MagicStarterLoginView; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Feature-screen preview: the magic_starter login view rendered backend-free. +class LoginScreenPreview extends StatelessWidget { + /// Creates the login screen preview. + const LoginScreenPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => const MagicStarterLoginView(); +} diff --git a/lib/preview/preview_mock_harness.dart b/lib/preview/preview_mock_harness.dart new file mode 100644 index 0000000..e52f2eb --- /dev/null +++ b/lib/preview/preview_mock_harness.dart @@ -0,0 +1,292 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart' show WidgetsBinding; +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart'; + +/// The render state a feature-screen preview should portray. +/// +/// Feature views are controller-backed, so the catalog drives them through the +/// network layer: [success] resolves canned data immediately, [loading] never +/// resolves (the view stays on its spinner), and [error] resolves a non-2xx +/// response so the view shows its error branch. No backend is ever contacted. +enum PreviewState { + /// Requests resolve with sample data so the view renders its filled state. + success, + + /// Requests never resolve so the view stays on its loading indicator. + loading, + + /// Requests resolve with a 500 so the view renders its error branch. + error, +} + +/// Sample backend-free data shared by every feature-screen preview. +/// +/// These maps mirror the `api/v1` resource shapes the real backend returns, so +/// the magic_starter views and controllers render exactly as they would in +/// production without a running server. +final class PreviewSampleData { + PreviewSampleData._(); + + /// A representative authenticated user. + static Map get user => { + 'id': 1, + 'name': 'Ada Lovelace', + 'email': 'ada@example.com', + 'email_verified_at': '2026-01-01T00:00:00.000Z', + 'two_factor_enabled': true, + 'current_team_id': 10, + 'profile_photo_url': null, + 'created_at': '2026-01-01T00:00:00.000Z', + }; + + /// A representative set of teams the sample user belongs to. + static List> get teams => >[ + { + 'id': 10, + 'name': 'Engineering', + 'personal_team': false, + 'role': 'owner', + }, + { + 'id': 11, + 'name': 'Design', + 'personal_team': false, + 'role': 'admin', + }, + { + 'id': 12, + 'name': 'Personal', + 'personal_team': true, + 'role': 'owner', + }, + ]; + + /// A representative notifications page. + static List> get notifications => >[ + { + 'id': 'n1', + 'type': 'team.invitation', + 'data': { + 'title': 'Team invitation', + 'body': 'You were invited to join Design.', + }, + 'read_at': null, + 'created_at': '2026-06-20T10:00:00.000Z', + }, + { + 'id': 'n2', + 'type': 'profile.updated', + 'data': { + 'title': 'Profile updated', + 'body': 'Your profile changes were saved.', + }, + 'read_at': '2026-06-19T08:00:00.000Z', + 'created_at': '2026-06-19T08:00:00.000Z', + }, + ]; +} + +/// A contract-mock network driver for the preview catalog. +/// +/// It mirrors the `MockNetworkDriver implements NetworkDriver` pattern the +/// magic_starter test suite uses (tracking [lastMethod]/[lastUrl]/[lastData]), +/// but instead of single-response queueing it answers every request from a +/// [PreviewState], so a preview can render loading, success, or error without a +/// backend. In [PreviewState.loading] requests return a [Future] that never +/// completes, leaving controller-backed views on their spinner. +final class PreviewMockNetworkDriver implements NetworkDriver { + /// Creates a driver answering every request according to [state]. + PreviewMockNetworkDriver(this.state); + + /// The state every response is shaped to portray. + final PreviewState state; + + /// The HTTP verb of the most recent request (diagnostic parity with tests). + String? lastMethod; + + /// The URL of the most recent request. + String? lastUrl; + + /// The payload of the most recent request. + dynamic lastData; + + /// Resolve a request for [url] to a canned response for the current [state]. + Future _respond(String method, String url, {dynamic data}) { + lastMethod = method; + lastUrl = url; + lastData = data; + + // 1. Loading: never resolve so the view stays on its loading indicator. + if (state == PreviewState.loading) { + return Completer().future; + } + + // 2. Error: a non-2xx response drives every view to its error branch. + if (state == PreviewState.error) { + return Future.delayed( + Duration.zero, + () => MagicResponse( + data: {'message': 'Preview error state.'}, + statusCode: 500, + message: 'Preview error state.', + ), + ); + } + + // 3. Success: route-shaped sample data. Resolved on the event queue + // (Future.delayed, not Future.value) so a controller that fires its + // fetch during the view's first build receives the response AFTER the + // frame; a synchronously-completed future would deliver the result + // mid-build and trip "setState() called during build" in the view. + return Future.delayed(Duration.zero, () => _successFor(url)); + } + + /// Build a 200 response whose body matches the resource [url]. + MagicResponse _successFor(String url) { + final body = {'data': _dataFor(url)}; + return MagicResponse(data: body, statusCode: 200); + } + + /// Pick the sample payload for a given [url]. + dynamic _dataFor(String url) { + if (url.contains('teams')) return PreviewSampleData.teams; + if (url.contains('notifications')) return PreviewSampleData.notifications; + if (url.contains('login') || url.contains('register')) { + return { + 'token': 'preview-token', + 'user': PreviewSampleData.user, + }; + } + return PreviewSampleData.user; + } + + @override + void addInterceptor(MagicNetworkInterceptor interceptor) {} + + @override + Future delete(String url, {Map? headers}) => + _respond('DELETE', url); + + @override + Future destroy( + String resource, + String id, { + Map? headers, + }) => _respond('DESTROY', '$resource/$id'); + + @override + Future get( + String url, { + Map? query, + Map? headers, + }) => _respond('GET', url); + + @override + Future index( + String resource, { + Map? filters, + Map? headers, + }) => _respond('INDEX', resource); + + @override + Future post( + String url, { + dynamic data, + Map? headers, + }) => _respond('POST', url, data: data); + + @override + Future put( + String url, { + dynamic data, + Map? headers, + }) => _respond('PUT', url, data: data); + + @override + Future show( + String resource, + String id, { + Map? headers, + }) => _respond('SHOW', '$resource/$id'); + + @override + Future store( + String resource, + Map data, { + Map? headers, + }) => _respond('STORE', resource, data: data); + + @override + Future update( + String resource, + String id, + Map data, { + Map? headers, + }) => _respond('UPDATE', '$resource/$id', data: data); + + @override + Future upload( + String url, { + required Map data, + required Map files, + Map? headers, + }) => _respond('UPLOAD', url, data: data); +} + +/// Installs a backend-free environment for the feature-screen previews. +/// +/// The catalog renders previews inside the already-booted app, so this harness +/// rebinds the `network` singleton to a [PreviewMockNetworkDriver] for the +/// requested [PreviewState] and seeds [Auth] with a sample user so +/// authenticated views (dashboard, profile, teams) render their filled state. +/// It is idempotent per-state: re-installing the same state is a no-op, so the +/// preview builders can call it on every `build()` without churning the +/// singleton or the auth session. +/// +/// This only ever runs from a `*.preview.dart` builder, which is reachable only +/// through the dev-only `/preview` boundary; it never touches a release build. +final class PreviewMockHarness { + PreviewMockHarness._(); + + static PreviewState? _installed; + + /// Bind the mock network for [state] and seed the sample auth session. + /// + /// Returns the bound driver so a caller can inspect the recorded request. + static PreviewMockNetworkDriver install(PreviewState state) { + final driver = PreviewMockNetworkDriver(state); + + // 1. Rebind the network layer so every controller request is mocked. + Magic.singleton('network', () => driver); + + // 2. Seed the authenticated session once so authenticated views render + // their filled state. Deferred to a post-frame callback: install() is + // called from a preview builder DURING the build phase, and Auth.login + // notifies its state listeners synchronously, which would trigger + // "setState() called during build". Running it after the frame seeds the + // session and rebuilds cleanly. Errors are swallowed deliberately: a + // preview must render even if the guard cannot persist a token (e.g. + // secure storage is unavailable on the preview platform). + if (_installed != state) { + _installed = state; + WidgetsBinding.instance.addPostFrameCallback( + (_) => unawaited(_seedAuth()), + ); + } + + return driver; + } + + /// Seed [Auth] with the sample user so authenticated previews render filled. + static Future _seedAuth() async { + try { + final user = MagicStarter.createUser(PreviewSampleData.user); + await Auth.login({'token': 'preview-token'}, user); + } catch (_) { + // Preview-only: a failed seed leaves the view in its guest/empty state, + // which is still a valid thing to preview. + } + } +} diff --git a/lib/preview/profile_screen.preview.dart b/lib/preview/profile_screen.preview.dart new file mode 100644 index 0000000..90ba246 --- /dev/null +++ b/lib/preview/profile_screen.preview.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic_starter/magic_starter.dart' + show MagicStarterProfileSettingsView; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Feature-screen preview: the profile settings view with a sample user, +/// rendered backend-free. +class ProfileScreenPreview extends StatelessWidget { + /// Creates the profile screen preview. + const ProfileScreenPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => + const MagicStarterProfileSettingsView(); +} diff --git a/lib/preview/register_screen.preview.dart b/lib/preview/register_screen.preview.dart new file mode 100644 index 0000000..62068d2 --- /dev/null +++ b/lib/preview/register_screen.preview.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic_starter/magic_starter.dart' show MagicStarterRegisterView; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Feature-screen preview: the magic_starter register view rendered +/// backend-free. +class RegisterScreenPreview extends StatelessWidget { + /// Creates the register screen preview. + const RegisterScreenPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => + const MagicStarterRegisterView(); +} diff --git a/lib/preview/screen_preview_scaffold.dart b/lib/preview/screen_preview_scaffold.dart new file mode 100644 index 0000000..e4a3f9d --- /dev/null +++ b/lib/preview/screen_preview_scaffold.dart @@ -0,0 +1,141 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic_starter/magic_starter.dart'; + +import 'preview_mock_harness.dart'; + +/// The navigation chrome a feature-screen preview renders inside. +/// +/// The app shell (`layout.app`) decides sidebar-vs-bottom-nav off the window +/// width (`wScreenIs` reads `MediaQuery.size`), and the two layouts cannot show +/// at once. To preview each independently the shell modes OVERRIDE the ambient +/// `MediaQuery` width, so a single catalog pane can pin one or the other. +enum PreviewChrome { + /// Bare: just the view content, vertically scrollable. Used for plain + /// screen-content previews and guest screens (no app navigation). + none, + + /// The authenticated app shell pinned to the DESKTOP layout: the sidebar + /// navigation rail + header + user menu, full pane width. + appDesktop, + + /// The authenticated app shell pinned to the MOBILE layout: the bottom + /// navigation bar + drawer, rendered in a phone-width frame. + appMobile, +} + +/// Shared scaffold for feature-screen previews. +/// +/// Installs the [PreviewMockHarness] for the requested [state] (so the wrapped +/// controller-backed view renders backend-free) and DEFERS the view mount by +/// one frame. +/// +/// The deferral matters: the catalog builds a preview's body synchronously +/// inside its own `build()`. A magic_starter feature view is controller-backed +/// (`MagicStatefulView`), and binding its controller on mount notifies state +/// listeners; mounting it DURING the catalog build trips "setState() called +/// during build". Mounting it one frame later (after a post-frame callback) +/// moves the controller bind out of the build phase, so the preview is clean. +/// The placeholder shown for that single frame keeps the layout from jumping. +class ScreenPreviewScaffold extends StatefulWidget { + /// Wraps [builder]'s output, installing the harness for [state] first. + const ScreenPreviewScaffold({ + super.key, + required this.state, + required this.builder, + this.chrome = PreviewChrome.none, + }); + + /// The state the harness should portray (success, loading, or error). + final PreviewState state; + + /// Builds the feature view to preview. + final WidgetBuilder builder; + + /// Which navigation chrome to render the view inside. + final PreviewChrome chrome; + + @override + State createState() => _ScreenPreviewScaffoldState(); +} + +class _ScreenPreviewScaffoldState extends State { + /// Bounded height for the app-shell modes. The app layout is a Scaffold with + /// `Expanded` regions and an anchored bottom nav, so it needs a finite + /// height; the catalog pane scrolls vertically (unbounded), so the shell gets + /// a representative viewport height and its content scrolls inside. + static const double _shellHeight = 760; + + /// Phone-frame width for [PreviewChrome.appMobile] (a common logical width). + static const double _phoneWidth = 390; + + /// Forced window width that keeps the shell on its desktop layout regardless + /// of the real browser width (>= the `lg` breakpoint). + static const double _desktopWidth = 1280; + + bool _mounted = false; + + @override + void initState() { + super.initState(); + // Bind the mock network + sample auth session for this state before the + // view mounts. Idempotent per-state, so it does not churn the singleton. + PreviewMockHarness.install(widget.state); + // Mount the controller-backed view one frame later (see the class doc): it + // must not build during the catalog's build phase. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() => _mounted = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!_mounted) { + // First frame: a sized placeholder so the column does not jump when the + // deferred view mounts. + return const SizedBox(height: 320); + } + + final Widget view = Builder(builder: widget.builder); + + switch (widget.chrome) { + case PreviewChrome.appDesktop: + // Sidebar layout, full pane width. The MediaQuery width is forced wide + // so `wScreenIs('lg')` stays true even on a narrow browser. + return SizedBox( + height: _shellHeight, + child: _withShell(context, width: _desktopWidth, child: view), + ); + case PreviewChrome.appMobile: + // Bottom-nav layout in a centered phone frame. The MediaQuery width is + // forced narrow so `wScreenIs('lg')` is false -> bottom nav + drawer. + return Center( + child: SizedBox( + width: _phoneWidth, + height: _shellHeight, + child: _withShell(context, width: _phoneWidth, child: view), + ), + ); + case PreviewChrome.none: + // Bare content at the catalog pane's natural width. No inner scroll + // view: the catalog page already scrolls vertically, and nesting a + // second vertical SingleChildScrollView here breaks the outer scroll + // geometry (and sidebar scroll-to-section). + return view; + } + } + + /// Wraps [child] in the authenticated app shell (`layout.app`) under a + /// [MediaQuery] whose width is overridden to [width], pinning the shell's + /// responsive sidebar-vs-bottom-nav decision for this preview. + Widget _withShell( + BuildContext context, { + required double width, + required Widget child, + }) { + final MediaQueryData base = MediaQuery.of(context); + return MediaQuery( + data: base.copyWith(size: Size(width, _shellHeight)), + child: MagicStarter.view.makeLayout('layout.app', child: child), + ); + } +} diff --git a/lib/preview/settings_screen.preview.dart b/lib/preview/settings_screen.preview.dart new file mode 100644 index 0000000..7185dbd --- /dev/null +++ b/lib/preview/settings_screen.preview.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic_starter/magic_starter.dart' + show MagicStarterTeamSettingsView; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Feature-screen preview: the team settings view, authenticated and +/// backend-free. +class SettingsScreenPreview extends StatelessWidget { + /// Creates the settings screen preview. + const SettingsScreenPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => + const MagicStarterTeamSettingsView(); +} diff --git a/lib/preview/sidebar_menu.preview.dart b/lib/preview/sidebar_menu.preview.dart new file mode 100644 index 0000000..4986746 --- /dev/null +++ b/lib/preview/sidebar_menu.preview.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic/magic.dart' show WDiv, WText; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Navigation preview: the authenticated app shell on its DESKTOP layout, the +/// sidebar navigation rail + header + user menu. The body is an empty +/// placeholder card so the preview focuses on the navigation chrome, not a +/// crammed screen. +class SidebarMenuPreview extends StatelessWidget { + /// Creates the sidebar-menu navigation preview. + const SidebarMenuPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + chrome: PreviewChrome.appDesktop, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => const WDiv( + className: 'p-6', + child: WDiv( + className: ''' + w-full h-80 rounded-xl + border border-color-border bg-surface-container + flex items-center justify-center + ''', + child: WText('Page content', className: 'text-fg-muted text-sm'), + ), + ); +} diff --git a/lib/preview/teams_screen.preview.dart b/lib/preview/teams_screen.preview.dart new file mode 100644 index 0000000..c852269 --- /dev/null +++ b/lib/preview/teams_screen.preview.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import 'package:magic_starter/magic_starter.dart' + show MagicStarterTeamCreateView; + +import 'preview_mock_harness.dart'; +import 'screen_preview_scaffold.dart'; + +/// Feature-screen preview: the team create view, authenticated and +/// backend-free. +class TeamsScreenPreview extends StatelessWidget { + /// Creates the teams screen preview. + const TeamsScreenPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const ScreenPreviewScaffold( + state: PreviewState.success, + builder: _build, + ); + } + + static Widget _build(BuildContext context) => + const MagicStarterTeamCreateView(); +} diff --git a/lib/resources/views/dashboard_view.dart b/lib/resources/views/dashboard_view.dart new file mode 100644 index 0000000..0a00d79 --- /dev/null +++ b/lib/resources/views/dashboard_view.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart' show Icons; +import 'package:flutter/widgets.dart'; +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart' + show Card, CardVariant, Typography, TypographyVariant; + +/// Dashboard view: the default landing page after successful authentication. +/// +/// Design-first: every surface and text color flows through the semantic +/// alias tokens (`bg-surface`, `text-fg`, ...) so it tracks DESIGN.md in both +/// light and dark. The quick-link tiles compose the shared [Card] component. +class DashboardView extends StatelessWidget { + /// Creates the [DashboardView]. + const DashboardView({super.key}); + + static const _iconHero = Icons.auto_awesome; + static const _iconDocs = Icons.menu_book; + static const _iconGitHub = Icons.code; + static const _iconCli = Icons.terminal; + static const _iconArrow = Icons.arrow_forward_outlined; + static const _iconHeart = Icons.favorite; + + @override + Widget build(BuildContext context) { + final appName = Config.get('app.name', 'My App') ?? 'My App'; + + return WDiv( + className: 'w-full max-w-[480px] md:max-w-4xl mx-auto p-4 lg:p-8', + child: WDiv( + className: ''' + rounded-2xl bg-surface-container + border border-color-border + p-6 lg:p-8 flex flex-col items-center + ''', + children: [ + // 1. Hero. + WDiv( + className: ''' + w-20 h-20 rounded-2xl + flex items-center justify-center + bg-primary + ''', + child: const WIcon( + _iconHero, + className: 'text-4xl text-on-primary', + ), + ), + const WSpacer(className: 'h-6'), + Typography( + appName, + variant: TypographyVariant.h2, + className: 'text-center', + ), + const WSpacer(className: 'h-2'), + const Typography( + 'Built with Magic Starter', + variant: TypographyVariant.caption, + ), + + const WSpacer(className: 'h-8'), + + // 2. Quick-link cards. + WDiv( + className: 'w-full grid grid-cols-1 md:grid-cols-3 gap-3', + children: [ + _buildLinkCard( + icon: _iconDocs, + title: 'Documentation', + description: 'Read the Magic Framework docs to get started.', + url: 'https://magic.fluttersdk.com', + ), + _buildLinkCard( + icon: _iconGitHub, + title: 'GitHub', + description: + 'Star the repo, report issues, or contribute code.', + url: 'https://github.com/fluttersdk/magic', + ), + _buildLinkCard( + icon: _iconCli, + title: 'CLI Commands', + description: + 'Run `magic --help` to see all available commands.', + url: 'https://magic.fluttersdk.com/cli', + ), + ], + ), + + const WSpacer(className: 'h-8'), + + // 3. Footer. + WDiv( + className: 'flex flex-row items-center justify-center gap-1', + children: [ + const WText('Made with', className: 'text-xs text-fg-muted'), + const WIcon(_iconHeart, className: 'text-xs text-destructive'), + const WText('by', className: 'text-xs text-fg-muted'), + WAnchor( + onTap: () => Launch.url('https://anilcancakir.com'), + child: const WText( + 'Anılcan Çakır', + className: 'text-xs font-medium text-fg', + ), + ), + ], + ), + ], + ), + ); + } + + /// Builds a single quick-link tile composing the shared [Card] component. + Widget _buildLinkCard({ + required IconData icon, + required String title, + required String description, + required String url, + }) { + return Card( + variant: CardVariant.inset, + child: WDiv( + className: 'flex flex-col', + children: [ + WDiv( + className: 'flex flex-row items-center gap-3 mb-2', + children: [ + WDiv( + className: 'p-2 rounded-lg bg-primary-container', + child: WIcon(icon, className: 'text-lg text-fg'), + ), + // flex-1 so the title takes the remaining row width and fits + // (or wraps) instead of overflowing when the card is narrow, + // e.g. inside the 3-column grid beside the app-shell sidebar. + WText(title, className: 'flex-1 text-base font-semibold text-fg'), + ], + ), + WText(description, className: 'text-sm text-fg-muted mb-3'), + WAnchor( + onTap: () => Launch.url(url), + child: WDiv( + className: 'flex flex-row items-center gap-1', + children: [ + const WText( + 'Learn more', + className: 'text-sm font-medium text-fg', + ), + const WIcon(_iconArrow, className: 'text-sm text-fg'), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/resources/views/welcome_view.dart b/lib/resources/views/welcome_view.dart new file mode 100644 index 0000000..69d36e9 --- /dev/null +++ b/lib/resources/views/welcome_view.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart' show Icons; +import 'package:flutter/widgets.dart'; +import 'package:magic/magic.dart'; +import 'package:magic_starter/magic_starter.dart' + show Card, CardVariant, Typography, TypographyVariant; + +/// Welcome view: the default landing page for a new Magic application. +/// +/// Design-first: colors flow through the semantic alias tokens so the screen +/// tracks DESIGN.md in light and dark. Kept as a self-contained centered shell +/// (it is the pre-auth landing, rendered outside the app layout). +class WelcomeView extends StatelessWidget { + /// Creates the [WelcomeView]. + const WelcomeView({super.key}); + + static const _iconHero = Icons.auto_awesome; + static const _iconDocs = Icons.menu_book; + static const _iconGitHub = Icons.code; + static const _iconCli = Icons.terminal; + static const _iconArrow = Icons.arrow_forward_outlined; + static const _iconHeart = Icons.favorite; + + @override + Widget build(BuildContext context) { + final appName = Config.get('app.name', '') ?? ''; + + return WDiv( + className: 'bg-surface min-h-screen w-full', + child: SingleChildScrollView( + child: WDiv( + className: 'w-full max-w-[480px] mx-auto p-4 lg:p-8', + child: WDiv( + className: ''' + rounded-2xl bg-surface-container + border border-color-border + p-6 lg:p-8 flex flex-col items-center + ''', + children: [ + // 1. Hero. + WDiv( + className: ''' + w-20 h-20 rounded-2xl + flex items-center justify-center + bg-primary + ''', + child: const WIcon( + _iconHero, + className: 'text-on-primary text-4xl', + ), + ), + const WSpacer(className: 'h-6'), + Typography( + appName, + variant: TypographyVariant.h2, + className: 'text-center', + ), + const WSpacer(className: 'h-2'), + const Typography( + 'Built with Magic Framework', + variant: TypographyVariant.caption, + ), + + const WSpacer(className: 'h-8'), + + // 2. Quick-link cards. + WDiv( + className: 'w-full flex flex-col gap-3', + children: [ + _buildLinkCard( + icon: _iconDocs, + title: 'Documentation', + description: + 'Read the Magic Framework docs to get started.', + url: 'https://magic.fluttersdk.com', + ), + _buildLinkCard( + icon: _iconGitHub, + title: 'GitHub', + description: + 'Star the repo, report issues, or contribute code.', + url: 'https://github.com/fluttersdk/magic', + ), + _buildLinkCard( + icon: _iconCli, + title: 'CLI Commands', + description: + 'Run `magic --help` to see all available commands.', + url: 'https://magic.fluttersdk.com/packages/magic-cli', + ), + ], + ), + + const WSpacer(className: 'h-8'), + + // 3. Footer. + WDiv( + className: 'flex flex-row items-center justify-center gap-1', + children: [ + const WText('Made with', className: 'text-xs text-fg-muted'), + const WIcon( + _iconHeart, + className: 'text-xs text-destructive', + ), + const WText('by', className: 'text-xs text-fg-muted'), + WAnchor( + onTap: () => Launch.url('https://anilcancakir.com'), + child: const WText( + 'Anılcan Çakır', + className: 'text-xs font-medium text-fg', + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Builds a single quick-link tile composing the shared [Card] component. + Widget _buildLinkCard({ + required IconData icon, + required String title, + required String description, + required String url, + }) { + return Card( + variant: CardVariant.inset, + child: WDiv( + className: 'flex flex-col', + children: [ + WDiv( + className: 'flex flex-row items-center gap-3 mb-2', + children: [ + WDiv( + className: 'p-2 rounded-lg bg-primary-container', + child: WIcon(icon, className: 'text-lg text-fg'), + ), + WText(title, className: 'text-base font-semibold text-fg'), + ], + ), + WText(description, className: 'text-sm text-fg-muted mb-3'), + WAnchor( + onTap: () => Launch.url(url), + child: WDiv( + className: 'flex flex-row items-center gap-1', + children: [ + const WText( + 'Learn more', + className: 'text-sm font-medium text-fg', + ), + const WIcon(_iconArrow, className: 'text-sm text-fg'), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/routes/app.dart b/lib/routes/app.dart new file mode 100644 index 0000000..2978d27 --- /dev/null +++ b/lib/routes/app.dart @@ -0,0 +1,25 @@ +import 'package:magic/magic.dart'; + +// import '../resources/views/welcome_view.dart'; // Replaced by DashboardView +import 'package:magic_starter/magic_starter.dart'; +import '../resources/views/dashboard_view.dart'; + +/// Application Route Definitions. +/// +/// Register all application routes here. This function is called by +/// [RouteServiceProvider.boot()] during the Magic bootstrap lifecycle. +/// +/// See also: `lib/app/kernel.dart` for middleware registration. +void registerAppRoutes() { + // Auth-protected routes with AppLayout + MagicRoute.group( + layout: (child) => MagicStarter.view.makeLayout('layout.app', child: child), + middleware: ['auth'], + layoutId: 'app', + routes: () { + MagicRoute.page('/', () => const DashboardView()); + }, + ); + + // MagicRoute.page('/', () => const WelcomeView()).title('Welcome'); // Replaced by DashboardView +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..834ca5c --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "magic_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.fluttersdk.magic_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..1f2a361 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_to_front_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); + window_to_front_plugin_register_with_registrar(window_to_front_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..db702e7 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,30 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window + file_selector_linux + flutter_secure_storage_linux + gtk + url_launcher_linux + window_to_front +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..47c9dff --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "magic_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "magic_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..4f5c312 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,32 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import desktop_webview_window +import file_picker +import file_selector_macos +import flutter_secure_storage_darwin +import flutter_web_auth_2 +import google_sign_in_ios +import share_plus +import shared_preferences_foundation +import url_launcher_macos +import window_to_front + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..06e701f --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* magic_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "magic_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* magic_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* magic_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/magic_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/magic_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/magic_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/magic_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/magic_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/magic_example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..85eee3f --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,77 @@ +{ + "pins" : [ + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1", + "version" : "11.3.0" + } + }, + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "a7caeda164dc5108bf4649472b28a5af65dc6f33", + "version" : "2.1.0" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "08d8dcecafb575f98879ffdbb8302c1b9ad65d19", + "version" : "9.2.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "c46e5f8b7c23265f17c24ca7f9fa1b13ded7a822", + "version" : "8.1.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "56e0ccf09a6dd29dc7e68bdf729598240ca8aa16", + "version" : "5.0.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" + } + } + ], + "version" : 2 +} diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..2342ab5 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..85eee3f --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,77 @@ +{ + "pins" : [ + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1", + "version" : "11.3.0" + } + }, + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "a7caeda164dc5108bf4649472b28a5af65dc6f33", + "version" : "2.1.0" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "08d8dcecafb575f98879ffdbb8302c1b9ad65d19", + "version" : "9.2.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "c46e5f8b7c23265f17c24ca7f9fa1b13ded7a822", + "version" : "8.1.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "56e0ccf09a6dd29dc7e68bdf729598240ca8aa16", + "version" : "5.0.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" + } + } + ], + "version" : 2 +} diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..5411389 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = magic_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.fluttersdk.magicExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.fluttersdk. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..d769771 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + keychain-access-groups + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..225aa48 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + keychain-access-groups + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..7f53d66 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1276 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: transitive + description: + name: app_links + sha256: "9d3c82f634c7f5b5c752f7ee46b67724246043f5e1d5fc1b433dd5b38d780dbe" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "78a18580eecac98108d1eef52a7db668bc317714f5205e616973363326efe333" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.dev" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dart_mcp: + dependency: transitive + description: + name: dart_mcp + sha256: c133c6f3b9668ed5e674bbf6f29a792f3abed9aeba53ec391c92f65f248acddf + url: "https://pub.dev" + source: hosted + version: "0.5.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + dio: + dependency: transitive + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + faker: + dependency: transitive + description: + name: faker + sha256: "544c34e9e1d322824156d5a8d451bc1bb778263b892aded24ec7ba77b0706624" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: transitive + description: + name: file_picker + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 + url: "https://pub.dev" + source: hosted + version: "11.0.2" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_dotenv: + dependency: transitive + description: + name: flutter_dotenv + sha256: d41da11fb497314fbf89811ec30af02d1d898b47980a129f0a8c0a1720460ba2 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" + url: "https://pub.dev" + source: hosted + version: "10.3.1" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttersdk_artisan: + dependency: "direct main" + description: + path: "../artisan" + relative: true + source: path + version: "0.0.8" + fluttersdk_dusk: + dependency: "direct main" + description: + path: "../dusk" + relative: true + source: path + version: "0.0.8" + fluttersdk_telescope: + dependency: "direct main" + description: + path: "../telescope" + relative: true + source: path + version: "0.0.4" + fluttersdk_wind: + dependency: "direct overridden" + description: + path: "../wind" + relative: true + source: path + version: "1.1.2" + fluttersdk_wind_diagnostics_contracts: + dependency: "direct overridden" + description: + path: "../wind_diagnostics_contracts" + relative: true + source: path + version: "1.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: transitive + description: + name: go_router + sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a" + url: "https://pub.dev" + source: hosted + version: "17.3.0" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + google_sign_in: + dependency: transitive + description: + name: google_sign_in + sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "5b89f1d3c095cc53dfa4f23fbfa88da06dff3fdeb1c86656f30cf8b4ca0e7af8" + url: "https://pub.dev" + source: hosted + version: "7.2.13" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: d473003eeca892f96a01a64fc803378be765071cb0c265ee872c7f8683245d14 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + url: "https://pub.dev" + source: hosted + version: "0.8.13+19" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + jiffy: + dependency: transitive + description: + name: jiffy + sha256: cabced5ddea4612f5e0c498df065ee8a1bd810f7ed757db910e95d2aba80161d + url: "https://pub.dev" + source: hosted + version: "6.4.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "82dfd37d3b2e5030ae4729e1d7f5538cbc45eb1c73d618b9272931facac3bec1" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logger: + dependency: transitive + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + magic: + dependency: "direct main" + description: + path: "../magic" + relative: true + source: path + version: "0.0.3" + magic_deeplink: + dependency: "direct main" + description: + path: "../magic_deeplink" + relative: true + source: path + version: "0.0.1" + magic_devtools: + dependency: "direct main" + description: + path: "../magic_devtools" + relative: true + source: path + version: "0.0.1" + magic_notifications: + dependency: "direct main" + description: + path: "../magic_notifications" + relative: true + source: path + version: "0.0.1" + magic_social_auth: + dependency: "direct main" + description: + path: "../magic_social_auth" + relative: true + source: path + version: "0.0.1" + magic_starter: + dependency: "direct main" + description: + path: "../magic_starter" + relative: true + source: path + version: "0.0.1-alpha.15" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + more: + dependency: transitive + description: + name: more + sha256: e252628d2183cc09539b686abfbd9d8302675959b89a2a8146f5f4baca6ac5ba + url: "https://pub.dev" + source: hosted + version: "4.7.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96 + url: "https://pub.dev" + source: hosted + version: "0.19.1" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + onesignal_flutter: + dependency: transitive + description: + name: onesignal_flutter + sha256: b0910a2585ca70f2ffa370c0878625781807fd301e89005868fbedb1e933dbbd + url: "https://pub.dev" + source: hosted + version: "5.6.3" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" + url: "https://pub.dev" + source: hosted + version: "12.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + url: "https://pub.dev" + source: hosted + version: "2.4.26" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad" + url: "https://pub.dev" + source: hosted + version: "3.3.3" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454" + url: "https://pub.dev" + source: hosted + version: "0.6.0+eol" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + timezone: + dependency: transitive + description: + name: timezone + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + url: "https://pub.dev" + source: hosted + version: "0.11.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.dev" + source: hosted + version: "2.2.4" +sdks: + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..ec43854 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,135 @@ +name: magic_example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.12.2 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # Local path links to the magic ecosystem under development in this workspace. + magic: + path: ../magic + magic_deeplink: + path: ../magic_deeplink + magic_notifications: + path: ../magic_notifications + magic_social_auth: + path: ../magic_social_auth + magic_starter: + path: ../magic_starter + + # Dev-tooling, imported by lib/main.dart under kDebugMode (release tree-shakes). + magic_devtools: + path: ../magic_devtools + fluttersdk_dusk: + path: ../dusk + fluttersdk_telescope: + path: ../telescope + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + fluttersdk_artisan: any + +# Force every reference (root and transitive) to the local workspace source so +# the plugins' own hosted magic constraints do not clash with the path links above. +dependency_overrides: + magic: + path: ../magic + magic_deeplink: + path: ../magic_deeplink + magic_notifications: + path: ../magic_notifications + magic_social_auth: + path: ../magic_social_auth + magic_starter: + path: ../magic_starter + magic_devtools: + path: ../magic_devtools + fluttersdk_dusk: + path: ../dusk + fluttersdk_telescope: + path: ../telescope + # Pin to the stable line: magic's ">=11.0.2 <13.0.0" otherwise lets the + # 12.0.0-beta prerelease resolve, whose non-null saveFile params break magic. + file_picker: ^11.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + assets: + - assets/lang/en.json + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..0b2bbdd --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,10 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:magic/magic.dart'; + +void main() { + testWidgets('Magic app boots smoke test', (WidgetTester tester) async { + await tester.pumpWidget(const MagicApplication(title: 'Test App')); + + expect(find.byType(MagicApplication), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..16a4584 --- /dev/null +++ b/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + magic_example + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..0471dad --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "magic_example", + "short_name": "magic_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/web/sqlite3.wasm b/web/sqlite3.wasm new file mode 100644 index 0000000..fbd7d8d Binary files /dev/null and b/web/sqlite3.wasm differ diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..abc9c59 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(magic_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "magic_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..9cb5cde --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,32 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowToFrontPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowToFrontPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..34b762f --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,31 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + desktop_webview_window + file_selector_windows + flutter_secure_storage_windows + share_plus + url_launcher_windows + window_to_front +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..98e2a3f --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.fluttersdk" "\0" + VALUE "FileDescription", "magic_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "magic_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.fluttersdk. All rights reserved." "\0" + VALUE "OriginalFilename", "magic_example.exe" "\0" + VALUE "ProductName", "magic_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..3524ff1 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"magic_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3cb7146 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_