Skip to content

Commit e89c61a

Browse files
committed
Readme split up
1 parent d9e1a3e commit e89c61a

69 files changed

Lines changed: 5233 additions & 4777 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Guidance for working in the **Shiny Controls** repo (`Shiny.Maui.Controls` + `Sh
88
- `samples/Sample/` — the MAUI + Blazor demo app. Each control has a feature page under `samples/Sample/Features/<Area>/`, wired into `AppShell.xaml` and `MauiProgram.cs`.
99
- `tests/` — unit tests.
1010
- `SKILLS/shiny-controls/` — the **local skill** (`SKILL.md` + one markdown file per control) that teaches code generation for these controls.
11-
- `README.md` — the package-level overview (top-of-file summary paragraph + per-control sections + NuGet badges).
11+
- `README.md` — the front door: summary paragraph, NuGet badges, Getting Started, and a grouped index linking to `docs/controls/`.
12+
- `docs/controls/`**one markdown file per control** (`datagrid.md`, `document-editor.md`, …), named to match `SKILLS/shiny-controls/` where a skill file exists, plus `styling.md` for the cross-cutting styling/theming note. Images are referenced as `../../assets/…`.
1213
- `themes/` — M3 theme pack seeds.
1314

1415
## Documentation site
@@ -24,7 +25,7 @@ The public docs live in a **separate repo**: `~/Desktop/dev/documentation` (Astr
2425

2526
With each fix and each new feature, update all of the following so they stay in sync:
2627

27-
1. **README.md** — reflect new/changed behavior; add a NuGet badge + section if it's a new package.
28+
1. **`docs/controls/<control>.md`** — reflect new/changed behavior in that control's own page (add the page and a row in the README's grouped index if the control is new). Touch `README.md` itself only for the summary paragraph, a NuGet badge, Getting Started, or the index.
2829
2. **Local skill** (`SKILLS/shiny-controls/`) — update the relevant control's `.md` (or add a new one and reference it in `SKILL.md`) so generated code matches.
2930
3. **Shiny docs** (`~/Desktop/dev/documentation`):
3031
- **Release notes** — add an entry to `src/content/docs/controls/release-notes.mdx`.
@@ -40,7 +41,7 @@ With each fix and each new feature, update all of the following so they stay in
4041
### Additionally, if the PACKAGE is new (or removed / renamed)
4142

4243
8. Add it to the solution (`Shiny.Controls.slnx`) and to `Build.slnf`.
43-
9. Add a NuGet badge to `README.md` (see step 1) — the badge block sits directly under the summary paragraph.
44+
9. Add a NuGet badge to `README.md` — the badge block sits directly under the summary paragraph — and mention the package in the summary paragraph itself.
4445
10. Add it to the **`Package` dropdown** in `.github/ISSUE_TEMPLATE/bug_report.yml` and to the **`Target Package`** dropdown in `.github/ISSUE_TEMPLATE/feature_request.yml`. Both dropdowns mirror `src/` exactly, so a removed or renamed package must come out of them too.
4546
11. Update the **Repo layout** bullet at the top of this file.
4647

README.md

Lines changed: 98 additions & 4773 deletions
Large diffs are not rendered by default.

SKILLS/shiny-controls/SKILL.md

Lines changed: 15 additions & 1 deletion
Large diffs are not rendered by default.

SKILLS/shiny-controls/captcha.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Captcha
2+
3+
A human check in front of a form. One component over four providers, chosen **by name at registration** and swapped without touching the markup.
4+
5+
| | |
6+
| --- | --- |
7+
| Host | **Blazor only** — WebAssembly, Server, Hybrid |
8+
| Package | `Shiny.Blazor.Controls` (core — no add-on) |
9+
| Namespace | `Shiny.Blazor.Controls.Captchas` — add it to the **app's** `_Imports.razor`; the package's own imports do not reach consumers |
10+
11+
**There is no MAUI `Captcha`.** The hosted providers are browser widgets — a script that renders into a DOM element — and the local challenge draws to an HTML canvas. Never emit `<Captcha>` in XAML. A MAUI app hosting Blazor in a `BlazorWebView` uses the local challenge as-is; the hosted providers need a real origin their site key is registered for.
12+
13+
## A token is not a verdict
14+
15+
The component hands you a **response token** in `State.Response`. For the hosted providers it is your **server** that posts that token, with your *secret* key, to the provider's siteverify endpoint. Trusting `IsSolved` alone is trusting the client, which is the thing a captcha exists to avoid.
16+
17+
Never put a secret key in the client project — `UseReCaptcha`/`UseHCaptcha`/`UseTurnstile` take the **public site key** only. If asked for "the captcha key", that is the one.
18+
19+
## Setup
20+
21+
Registration is **optional**. With nothing registered, `<Captcha />` renders the local challenge with its defaults.
22+
23+
```csharp
24+
// umbrella
25+
builder.Services.AddShinyControls(cfg => cfg
26+
.ConfigureCaptcha(c => c.UseTurnstile("0x4AAA..."))
27+
);
28+
29+
// or on its own
30+
builder.Services.AddShinyCaptcha(c => c.UseTurnstile("0x4AAA..."));
31+
```
32+
33+
Register as many as you like — the component picks one by name, and absent a name uses `SetDefaultProvider`, then the first registered, then the local challenge. That is what makes "Turnstile in production, local challenge in the dev build" a config change rather than a markup change.
34+
35+
```csharp
36+
builder.Services.AddShinyCaptcha(c => c
37+
.UseTurnstile(siteKey)
38+
.UseLocal(o => o.Mode = LocalCaptchaMode.Math, name: "math") // a second, named local challenge
39+
.SetDefaultProvider("turnstile")
40+
.SetTheme(CaptchaTheme.Auto)
41+
.SetSize(CaptchaSize.Normal)
42+
.SetLanguage("fr")
43+
);
44+
```
45+
46+
| Registration | Provider name | Notes |
47+
| --- | --- | --- |
48+
| `UseLocal(configure?, name?)` | `local`, or the name given | Self-hosted; the un-named one is also the fallback |
49+
| `UseReCaptcha(siteKey)` | `recaptcha` | Google |
50+
| `UseHCaptcha(siteKey)` | `hcaptcha` | |
51+
| `UseTurnstile(siteKey)` | `turnstile` | Cloudflare; the only one supporting `Flexible` |
52+
| `UseProvider<T>()` / `UseProvider(instance)` | whatever `Name` returns | |
53+
54+
A `Provider="..."` naming something that is **not** registered renders a visible "No captcha provider named … is registered" alert rather than silently dropping to a weaker check. That is a wiring mistake, not a fallback.
55+
56+
## Usage
57+
58+
```razor
59+
<Captcha @ref="captcha" ValidChanged="v => canSubmit = v" />
60+
61+
<button disabled="@(!canSubmit)" @onclick="SubmitAsync">Sign up</button>
62+
63+
@code {
64+
Captcha? captcha;
65+
bool canSubmit;
66+
67+
async Task SubmitAsync()
68+
{
69+
var token = this.captcha!.Response; // hand this to your server
70+
// ... post the form ...
71+
72+
// a spent token cannot be replayed — start a fresh challenge after a failed submit
73+
await this.captcha.ResetAsync();
74+
}
75+
}
76+
```
77+
78+
`ValidChanged` is the property to gate a submit button on. It flips both ways — a solved challenge that later **expires** flips it back — which polling `IsSolved` on render would miss.
79+
80+
## Server-side validation
81+
82+
`Validate` is called with the fresh token the moment the widget solves, and decides whether the state counts as valid. Return `false` and the component stays invalid — and, unless `ResetOnFailedValidation="false"`, throws the challenge away and starts a new one, because a token your server rejected is spent either way. A `Validate` that **throws** is treated as a rejection and the exception message is shown as the widget error.
83+
84+
```razor
85+
<Captcha Validate="VerifyAsync" Solved="OnSolved" />
86+
87+
@code {
88+
void OnSolved(CaptchaState state) { /* Valid is already true here */ }
89+
90+
async Task<bool> VerifyAsync(CaptchaState state)
91+
{
92+
// your endpoint holds the secret key and posts to the provider's siteverify
93+
var response = await http.PostAsJsonAsync("api/captcha/verify", new { state.Response });
94+
return response.IsSuccessStatusCode;
95+
}
96+
}
97+
```
98+
99+
`Solved` fires **after** `Validate` has agreed, so a handler on it can assume the check passed.
100+
101+
## Invisible mode
102+
103+
An invisible provider scores the session in the background and renders no challenge, so nothing ever solves on its own. Call `ExecuteAsync()` from the submit handler and continue in `Solved`.
104+
105+
```razor
106+
<Captcha @ref="captcha" Size="CaptchaSize.Invisible" Solved="OnSolvedAsync" />
107+
108+
@code {
109+
Task SubmitAsync() => this.captcha!.ExecuteAsync(); // work continues in OnSolvedAsync
110+
}
111+
```
112+
113+
`BadgePosition` places the provider's badge — `BottomEnd` (default), `BottomStart`, or `Inline` to render it in the flow. The local provider has nothing to score and ignores `Invisible` entirely; `ExecuteAsync()` is a no-op for visible widgets.
114+
115+
## The local challenge
116+
117+
Self-hosted: no account, no site key, no third-party script, works offline and inside a `BlazorWebView`.
118+
119+
**Say this when suggesting it:** it is a speed bump, not a security boundary. The challenge is generated *and checked* in the browser, so anything with a debugger attached can read the answer out of memory. It stops naive form-fill bots and nothing more. For a public form worth attacking, register a hosted provider and verify the token on the server.
120+
121+
```csharp
122+
builder.Services.AddShinyCaptcha(c => c.UseLocal(o =>
123+
{
124+
o.Mode = LocalCaptchaMode.Math;
125+
o.ExpirySeconds = 60;
126+
}));
127+
```
128+
129+
| Option | Default | |
130+
| --- | --- | --- |
131+
| `Mode` | `Text` | `Text` draws distorted characters to a canvas; `Math` asks a small sum |
132+
| `Length` | `5` | Characters in the text challenge, clamped to 3–12 |
133+
| `CharacterSet` | `ABCDEFGHJKMNPQRSTUVWXYZ23456789` | Look-alikes (`0 O 1 I L`) already removed |
134+
| `CaseSensitive` | `false` | |
135+
| `Width` / `Height` | `180` / `60` | Canvas size in CSS pixels |
136+
| `ExpirySeconds` | `120` | `Expired` then fires and the widget resets. `0` or less disables expiry |
137+
| `MaxAttempts` | `3` | Wrong answers before the challenge is redrawn |
138+
| `Prompt` / `IncorrectText` / `RefreshText` / `PlaceholderText` || Wording, for localisation |
139+
140+
`LocalCaptchaMode.Math` renders **real text**, not a canvas, so a screen reader can read it — that is the whole reason it exists. Prefer it, or offer it alongside, whenever accessibility is in scope. The answer is checked as soon as enough characters are typed (Enter forces a check), a wrong answer shakes the field, and `MaxAttempts` wrong answers redraw the challenge.
141+
142+
Its token is an opaque `local.<guid>` — the shape matches the hosted providers so the calling code does not change, but it proves only that *this browser* solved the challenge. There is nothing to verify it against, so do not write a `Validate` that pretends to.
143+
144+
## Parameters
145+
146+
| Parameter | Type | Default | |
147+
| --- | --- | --- | --- |
148+
| `Provider` | `string?` | `null` | Which registered provider. Null follows the configured default |
149+
| `Theme` | `CaptchaTheme?` | configured | `Auto` (from `prefers-color-scheme`), `Light`, `Dark` |
150+
| `Size` | `CaptchaSize?` | configured | `Normal`, `Compact`, `Invisible`, `Flexible` (**Turnstile only**, falls back to `Normal`) |
151+
| `LanguageCode` | `string?` | `null` | Two-letter code. Null follows the browser |
152+
| `BadgePosition` | `CaptchaBadgePosition` | `BottomEnd` | Invisible mode only |
153+
| `ShowError` | `bool` | `true` | Renders widget failures under the widget |
154+
| `ResetOnFailedValidation` | `bool` | `true` | |
155+
| `Validate` | `Func<CaptchaState, Task<bool>>?` | `null` | |
156+
| `CssClass` | `string?` | `null` | Extra classes on the host element; unmatched attributes splat onto it too |
157+
158+
**Events**`Solved(CaptchaState)`, `Expired`, `Errored(string)` (script blocked, bad site key, network gone), `ValidChanged(bool)`.
159+
160+
**Members**`State` (never null), `IsSolved`, `Response`, `ResetAsync()`, `ExecuteAsync()`.
161+
162+
`CaptchaState` is a record: `(bool Valid, string? Response, string ProviderName)`.
163+
164+
## A provider the package does not ship
165+
166+
The built-ins are only `ICaptchaProvider` implementations registered by name. For another hosted one, subclass `RemoteCaptchaProvider` and supply a descriptor — one shared JS driver does script loading, widget lifetime, callbacks, reset and execute.
167+
168+
```csharp
169+
public class MyCaptchaProvider(string siteKey) : RemoteCaptchaProvider
170+
{
171+
public override RemoteCaptchaDescriptor Descriptor { get; } = new()
172+
{
173+
Name = "mycaptcha",
174+
ScriptUrl = "https://example.com/api.js?render=explicit{lang}", // {lang} is substituted or dropped
175+
GlobalName = "mycaptcha",
176+
SiteKey = siteKey,
177+
UseReadyCallback = false, // true when the global exposes ready(cb), as reCAPTCHA does
178+
SupportsBadge = true, // render() takes a badge option
179+
LanguageAsRenderOption = false, // true puts the language in render() instead of the URL
180+
SupportedSizes = ["normal", "compact"]
181+
};
182+
}
183+
184+
builder.Services.AddShinyCaptcha(c => c.UseProvider(new MyCaptchaProvider(siteKey)));
185+
```
186+
187+
For something that is not a script-and-global widget at all, implement `ICaptchaProvider` directly: return a `RenderFragment` from `Render(CaptchaRenderContext)`, raise `OnSolved`/`OnExpired`/`OnErrored`, and call `OnWidgetReady(this)` with an `ICaptchaWidget` so `ResetAsync`/`ExecuteAsync` have something to talk to.
188+
189+
## Gotchas
190+
191+
- **Never ship a secret key to the client.** Site key in the app, secret key on the server.
192+
- `IsSolved` alone is not verification for a hosted provider — pair it with `Validate` and a siteverify call.
193+
- Reset after a failed submit. A token is single-use at the provider.
194+
- Bind a submit button to `ValidChanged`, not to a value read once — challenges expire.
195+
- The local challenge is client-side only; do not present it as protection for a public form.
196+
- Blazor only. No `<Captcha>` in XAML, no `UseShinyCaptcha()` on `MauiAppBuilder`.

docs/controls/address-entry.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# AddressEntry
2+
3+
[← All Shiny Controls](../../README.md)
4+
5+
An address search control built on AutoCompleteEntry that queries a geocoding provider (Nominatim/OpenStreetMap by default). Returns structured address data with coordinates.
6+
7+
```xml
8+
<shiny:AddressEntry SelectedAddress="{Binding Address}"
9+
Placeholder="Search address..."
10+
CountryCodes="us,ca"
11+
FontSize="16" />
12+
```
13+
14+
| Property | Type | Default | Description |
15+
|---|---|---|---|
16+
| SelectedAddress | Address | null | Selected address (TwoWay) |
17+
| SearchProvider | IAddressSearchProvider? | null | Custom search provider (defaults to Nominatim) |
18+
| CountryCodes | string? | null | Comma-separated ISO country codes to filter results |
19+
| Placeholder | string | "Search address..." | Placeholder text |
20+
| MaxDropDownHeight | double | 250 | Max dropdown height |
21+
| TextColor | Color/string | null | Text color |
22+
| PlaceholderColor | Color/string | null | Placeholder color |
23+
| DropDownBackgroundColor | Color/string | null | Dropdown background |
24+
| DropDownBorderColor | Color/string | null | Dropdown border color |
25+
| FontSize | double | 14 | Font size |
26+
| FontFamily | string? | null | Font family (MAUI only) |
27+
| CornerRadius | double | 4 | Dropdown corner radius (MAUI only) |
28+
| InputClass | string? | null | Input CSS class (Blazor only) |
29+
| DropDownClass | string? | null | Dropdown CSS class (Blazor only) |
30+
31+
Events: `AddressSelected` fires when an address is chosen.
32+
33+
The `Address` record provides: `DisplayName`, `HouseNumber`, `Street`, `City`, `State`, `PostalCode`, `Country`, `CountryCode`, `Latitude`, `Longitude`.
34+
35+
Implement `IAddressSearchProvider` for custom geocoding:
36+
37+
```csharp
38+
public class MyGeoProvider : IAddressSearchProvider
39+
{
40+
public Task<IList<Address>> SearchAsync(string query, string? countryCodes, CancellationToken ct)
41+
{
42+
// call your preferred geocoding API
43+
}
44+
}
45+
```

docs/controls/autocomplete.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# AutoCompleteEntry
2+
3+
[← All Shiny Controls](../../README.md)
4+
5+
A text input with debounced search, dropdown suggestions, busy indicator, and custom item templates. Supports both local filtering and remote search via a command/callback. Available on both MAUI and Blazor with full styling control.
6+
7+
![AutoCompleteEntry](../../assets/autocomplete1.png)
8+
9+
```xml
10+
<shiny:AutoCompleteEntry
11+
Text="{Binding SearchText}"
12+
Placeholder="Search..."
13+
ItemsSource="{Binding Results}"
14+
SelectedItem="{Binding SelectedResult}"
15+
SearchCommand="{Binding SearchCommand}"
16+
TextMemberPath="Name"
17+
DebounceInterval="300"
18+
Threshold="2"
19+
MaxDropDownHeight="250"
20+
FontSize="16"
21+
TextColor="Black"
22+
DropDownBackgroundColor="White"
23+
DropDownBorderColor="LightGray"
24+
CornerRadius="8" />
25+
```
26+
27+
| Property | Type | Default | Description |
28+
|---|---|---|---|
29+
| Text | string | "" | Current text value (TwoWay) |
30+
| Placeholder | string? | null | Placeholder text |
31+
| PlaceholderColor | Color/string | null | Placeholder text color |
32+
| ItemsSource | IList | null | Suggestion items |
33+
| SelectedItem | object? | null | Currently selected item (TwoWay) |
34+
| SearchCommand | ICommand / EventCallback\<string\> | null | Remote search command |
35+
| TextMemberPath | string? | null | Property name to display from items |
36+
| ItemTemplate | DataTemplate / RenderFragment\<object\> | null | Custom dropdown item template |
37+
| IsBusy | bool | false | Show/hide the loading spinner (TwoWay) |
38+
| DebounceInterval | int | 300 | Debounce delay (ms) |
39+
| Threshold | int | 1 | Minimum characters before searching |
40+
| MaxDropDownHeight | double | 200 | Maximum dropdown height (px) |
41+
| TextColor | Color/string | null | Input text color |
42+
| FontSize | double | 14 | Input font size |
43+
| FontFamily | string? | null | Input font family (MAUI only) |
44+
| FontAttributes | FontAttributes | None | Bold/italic (MAUI only) |
45+
| DropDownBackgroundColor | Color/string | White | Dropdown background |
46+
| DropDownBorderColor | Color/string | LightGray | Dropdown border color |
47+
| CornerRadius | double | 4 | Dropdown border radius (MAUI only) |
48+
| SpinnerColor | Color/string | Grey | Loading spinner color |
49+
| CssClass | string? | null | Root CSS class (Blazor only) |
50+
| InputClass | string? | null | Input element CSS class (Blazor only) |
51+
| DropDownClass | string? | null | Dropdown CSS class (Blazor only) |
52+
| AdditionalAttributes | IDictionary | null | Unmatched HTML attributes (Blazor only) |
53+
54+
Events: `ItemSelected` fires when a suggestion is chosen.
55+
56+
**Blazor CSS Custom Properties** — Override these on a parent element or the component itself to theme without parameters:
57+
58+
| Variable | Default | Controls |
59+
|---|---|---|
60+
| `--shiny-ac-text` | inherit | Input text color |
61+
| `--shiny-ac-ph` | #9CA3AF | Placeholder color |
62+
| `--shiny-ac-dd-bg` | #fff | Dropdown background |
63+
| `--shiny-ac-dd-border` | #D1D5DB | Dropdown border |
64+
| `--shiny-ac-spinner` | #9CA3AF | Spinner color |
65+
| `--shiny-ac-font-size` | inherit | Input font size |
66+
| `--shiny-ac-dd-max-h` | 200px | Dropdown max height |

0 commit comments

Comments
 (0)