flocks 0.1.2
flocks: ^0.1.2 copied to clipboard
Flocks — a Flutter design system built on widgets.dart: design tokens, theme and brand (white-label).
Changelog #
Every relevant change to this package. The format follows Keep a Changelog, and the numbering follows SemVer.
On the numbering. Nothing below
[0.1.0]was ever published. Those versions are internal migration milestones from the monorepo — they ran from0.1.0to1.5.0while the pubspec still said1.0.0, so they never agreed with it in the first place. The component catalog used to carry the same milestones in asince:field; it was dropped for that reason.The public line starts at
0.1.0, on purpose.1.0.0on pub.dev is a promise about the future, and this package has not yet met an external consumer. In0.x, SemVer makes the minor the breaking slot, so^0.1.0pins adopters to>=0.1.0 <0.2.0and the churn cannot reach them by surprise. It graduates to1.0.0once the API holds still through a few outside adopters.
0.1.2 - 2026-08-12 #
A marca aprende a se escrever. É a única feature de pacote que o ROADMAP prevê, e ela existe para a demo do site: sem isto o visitante vê a marca dele nos 131 componentes e vai embora; com isto ele leva o arquivo que reproduz o que viu.
E a demo já pagou o primeiro dividendo: montar duas telas inteiras sobre o eixo
de forma revelou que uma superfície grande em circular cortava o próprio
conteúdo — um defeito que nenhum use case isolado tinha exercitado.
Added #
-
toDartSnippetescreve umaAppBrandConfigcomo código Dart colável. É uma extension, não um método: a classe documenta no próprio dartdoc o que ela deliberadamente não guarda, e gerar código não é responsabilidade de uma configuração. Também não é umCodec— o nome vem dedart:converte promete umdecodesimétrico que não existe aqui, porque o decode desta serialização é o compilador Dart.A saída traz só o que difere do padrão: eixo em
standardnão aparece, papel de cor ausente também não. Cada swatch sai pela função que o reconstrói (swatchFromSeed,neutralSwatchFromSeed,flippedSwatch) sempre que a semente o reconstrói de fato — o gerador confere antes de escolher a forma curta, e um swatch escrito à mão, que não tem semente que o descreva, cai no literal de 11 stops. Serialização que não faz ida e volta é serialização errada, e a alternativa (assumir a semente) devolveria uma paleta diferente da que entrou.O gate mora em
test/architecture/brand_snippet_freshness_test.darte o artefato emtest/support/exported_brand_snippet.dart— um.dartde verdade, e não um.txt, para que odart analyzeda raiz prove de graça aquilo que um teste de string não alcança: que o snippet compila. -
flippedSwatchespelha um swatch, que é como a rampa neutra escura de uma marca se obtém da clara. A função já existia, privada, dentro daflocksBrand; virou pública porque um snippet que a usasse sem poder nomeá-la teria de despejar os 11 stops justamente no papel onde a saída precisa continuar legível.kSwatchStopsveio junto, pelo mesmo motivo de quem percorre um swatch: umColorSwatchnão expõe as próprias chaves. -
A IBM Plex Mono, empacotada —
AppFontFamilies.ibmPlexMono, pesos 400 e 600, com oOFL.txtao lado dos.ttfcomo as outras duas famílias. São os dois pesos que o pacote realmente pede: 400 em todo bloco e código inline, 600 nos placeholders deAppApiPath. Sem itálico, porque ocase 'code'do inline builder não herda o estilo do pai e mono itálica nunca é pedida.AppContentStyle.codedeixa de tentar a mono do SO, e isso conserta duas coisas de uma vez. As baselines de código passam a mostrar código: no sandbox de teste nenhuma mono de sistema existe, então oflutter_test_config.dartregistrava a Poppins sob o nomeSF Mono— determinístico, sim, mas era uma proporcional fingindo ser mono, e é isso que os 4 goldens deAppMarkdowntinham de errado. O que ainda varia entre máquinas é o rasterizador de texto, e isso nunca foi sobre a família. E, no CanvasKit, uma pilha de famílias não registradas faz cada codepoint sem cobertura entrar na fila de fallback do engine: um acento de comentário em português bastava para baixar 69.116 B de Noto Sans Symbols defonts.gstatic.com(medido em produção na PR #20, e medido de novo aqui nos dois builds servidos lado a lado — 20 requisições e 2 de terceiro antes, 21 e 1 depois). A Roboto de 63.464 B continua: a condição que a dispara é o nome da família noFontManifest.json, e nada disto muda isso.Custa 280.252 B nos assets — 135.580 do Regular, 140.216 do SemiBold, 4.456 do
OFL.txt— e custa integralmente: fonte de texto declarada emfonts:não é podada pelo--tree-shake-icons, que só alcança fonte de ícone porIconDataconstante, e os dois.ttfsaem do build web com os mesmos bytes com e sem a flag. No tarball comprimido isso vira algo perto de +8%, e não um número exato: odart pub publish --dry-runsó reporta "1 MB", e reproduzir o arquivo por fora dá de 1,52 a 1,63 MB para a MESMA lista de caminhos, conforme o empacotador e o nível de gzip. O que é exato é o que está acima. A Inconsolata faria o mesmo trabalho por 63.332 B menos; ficou de fora por cobertura, e o pubspec registra a comparação inteira.Junto entra
test/architecture/font_axis_test.dart, que cobra o que até aqui era só comentário: todoasset:declarado existe em disco, o carregador dos testes espelha a seçãofonts:do pubspec, e o texto da licença viaja no diretório da família que ele cobre.Depreciadas por isto:
kAppContentMonoFamilyekAppContentMonoFallback. Eram a pilha de monos do sistema e não têm mais função, mas eram API pública na 0.1.1 — e em0.xo slot de mudança breaking é o minor, então ficam marcadas com@Deprecatede saem na 0.2.0.
Fixed #
-
Uma superfície grande em
circularparou de cortar o próprio conteúdo.AppCarde o cartão de conteúdo doAppShellresolviam a forma pela escada GERAL, em quecircularsignifica "metade do lado menor". Num chip isso é a pílula que se espera; num cartão de gráfico de 400 px é uma elipse de 180 px de raio cujo canto passa por cima do header — o título "Recurring revenue" aparecia como "urring revenue". Os dois passaram a usarcontentSurfaceRadius, uma escada nova que fica ENTREresolveesurfaceCornerRadius: idêntica à geral emreto/redondo/padrao(um cartão pequeno não deve herdar o canto de um bottom sheet) e com teto emcircular.O gate é
test/architecture/surface_clip_test.dart, e ele é geométrico em vez de tipográfico: pinta a área de conteúdo e exige que os quatro cantos dela continuem pintados. Não depende de fonte, tema nem baseline — só do clip.Descoberto pela demo da Fase D, que é a primeira coisa a exercitar o eixo de forma inteiro com conteúdo real em cima.
-
Texto decorativo parou de interceptar seleção sobre área interativa na web.
AppTextembrulha todo texto numAppSelectionRegion, e com umOverlayancestral isso vira umSelectableRegion. Na web umSelectableRegionmonta umPlatformSelectableRegionContextMenu— umPositioned.fillcomHtmlElementView, ou seja um elemento real do DOM cobrindo a região. Um elemento do DOM não participa do hit-test do Flutter, então oIgnorePointerque marcava o texto como decorativo desaparecia para o framework e continuava de pé para o navegador: a dica do campo, os rótulos de eixo dos gráficos e o número do passo do stepper ofereciam seleção e menu de contexto exatamente sobre o que devia ser só alvo de interação. Medido: 21 desses divs numa tela da demo, e 48 pontos da área de umAppBarChartcom um deles no topo.Os 17 sítios de texto decorativo passaram a viver num
SelectionContainer.disabled, que zera o registrar e cai no guard queAppSelectionRegionjá tinha — sem API pública nova, e pela receita que o pacote já usava em 33 arquivos (oAppAvatara documenta citando oButtonCore). AtingeAppInput,AppColorPickerInput,AppSteppere os cinco gráficos, que eram justamente os que tinham ficado de fora.O gate é
test/architecture/decorative_selection_test.dart, e é de FONTE por necessidade: na VM o platform view nem chega a ser construído — oSelectableRegionsó o embrulha sobkIsWeb && BrowserContextMenu.enablede fora de Android/iOS, e o ramo_iodo arquivo existe apenas para o import condicional compilar (descarta ochilde lançaUnimplementedErrornobuild). Nenhum teste de widget alcança a classe. O gate resolve tipos que carregam texto transitivamente e filhos passados por variável, porque as duas formas escaparam da primeira versão da regra. -
O
Overlayque o pacote exige passou a estar no caminho de entrada. Os componentes que flutuam inserem noOverlayancestral mais próximo — os quatro dropdowns, oAppTooltip, oshowAppOverlay(e oshowAppSnackbarque sai dele) e o controlador ancorado que serveAppPopover,AppMenu,AppOmniSearche oAppPickerAnchordos campos de data, hora e cor. O pacote não montaOverlayem lugar nenhum, de propósito: quem hospeda decide onde a camada flutuante vive. O mecanismo já aparecia no dartdoc de alguns deles ("renderiza viaOverlay"), mas dizer por onde o painel sai não é dizer que o host tem de fornecer o ancestral, nem o que acontece quando falta — e o README, que é a página do pub.dev, não citava a palavra uma vez. Oexample/lib/main.dartia além: montava um root SEMOverlay, então quem copiasse o exemplo e pusesse um dropdown recebia o crash. Os dois passaram a montar e a nomear quem exige, junto com as duas páginas da landing, que traziam o mesmorunApp.Nenhum comportamento mudou POR CAUSA deste item — nada em
lib/foi tocado por ele, e atualizar não conserta o crash de quem já o tem: o que faltava era a frase, não o código. Sem ancestral, abrir qualquer um deles lança em debug pelo assert do próprio framework, que manda incluirMaterialApp,CupertinoAppouNavigator— os três widgets que este pacote existe para não ter. AErrorDescriptiondele fica genérica ("Some widgets require an Overlay widget ancestor") porque nenhum dos sítios passadebugRequiredFor; oErrorSummaryacima dela nunca nomeia nada, e o componente que falhou aparece só na última linha, a do contexto. Em release o assert sai, oOverlay.oftermina no!dele e restaNull check operator used on a null value.A exigência alcança mais que os sítios diretos, e o README passou a dizer as três categorias: quem insere sozinho, quem herda de um filho (
AppInput(info:),AppPagination(perPage:),AppSplitButton) e todo campo de texto, por regra do framework — ao GANHAR FOCO, umAppInputmonta umTextSelectionOverlay, e é o construtor doSelectionOverlayembaixo dele que exige o ancestral, comassert(debugCheckHasOverlay(context))na lista de inicialização (widgets/text_selection.dart). O toque é só uma das formas:Tabe umrequestFocus()nofocusNodeque o campo aceita por parâmetro lançam igual. Desenhar um campo não lança porque até o foco o overlay de seleção não existe.O gate novo é de PRESENÇA, e cobre as quatro cópias da mesma raiz: o primeiro bloco de código do README e o
example/lib/main.dartnoreadme_example_test.dart, que já lia o README, e o<pre><code>das duas páginas do site noinstall_docs_test.dart, ao lado da copy de instalação que ele já fiscalizava. O critério está emtest/architecture/entry_root_tokens.dart. Fecha uma armadilha silenciosa: sem ele oOverlaysai de uma das quatro numa refatoração e nada fica vermelho — nem odart analyze, porque a árvore sem ele compila. Nas páginas a comparação é sobre o texto extraído do DOM, porque no HTML cru o token vem partido pelos<span>de coloração. A árvore da demo continua coberta pelooverlay_dependent_test.dartdela.
0.1.1 - 2026-08-10 #
Três defeitos que só a análise do pub.dev revelou, no dia seguinte à
publicação. Nenhum deles se conserta na 0.1.0: lá o tarball é imutável.
Fixed #
- A licença volta a ser reconhecida. O
LICENSEtrazia o MIT verbatim seguido de um bloco de notas sobre assets de terceiros, e olicense_detectorcasa o arquivo INTEIRO contra o corpus SPDX — o texto apensado derrubava a confiança abaixo do limiar e o pub.dev reportava "No license was recognized", 0 de 10 pontos. O arquivo passa a ser exatamente o texto SPDX. As notas de terceiros continuam no README, e as obrigações legais sempre estiveram cumpridas pelos textos que viajam ao lado de cada asset (OFL.txt,assets/icons/LICENSE). - O pacote compila em WebAssembly.
app_network_icon_provider.dartescolhia o ramo do loader de ícones comif (dart.library.html), edart:htmlnão existe no dart2wasm: todo build--wasmcaía no ramo default e arrastavadart:io(viaflutter_cache_manager) para um alvo que não o tem. A condição virouif (dart.library.io), que é verdadeira na VM e falsa nos dois backends web. Não era só nota: um app em wasm quebrava. - Suporte de plataforma volta a 6 de 6. Ver abaixo.
Changed #
-
A interceptação de ponteiro passou a morar no pacote, em
src/foundation/pointer/, e a dependênciapointer_interceptorsaiu. Ela era um plugin federado que endossa sówebeios, e o pana intersecta as plataformas de todo o fecho de dependências: aquela única linha rebaixava oflocks— e por herança oflocks_phosphore oflocks_material— a "Supports 2 of 6 platforms (iOS, Web)" na página do pub.dev, num design system que roda em toda parte. Import condicional não resolveria: o pana lê o pubspec, não o grafo de imports.No web o comportamento é o mesmo, pelo mesmo mecanismo (um
<div>vazio montado atrás do conteúdo, agora sobredart:js_interop+package:web, e portanto wasm-compatível). O que se perdeu foi a interceptação no iOS: ela dependia de umUIViewnativo, e código nativo é justamente o que um pacote Dart puro não pode carregar sem virar plugin — que é o problema que se estava resolvendo. Um app iOS que precise disso pode declararpointer_interceptorpor conta própria e embrulhar oAppOverlayCard.A API pública de
AppOverlayCardnão mudou: mesmos parâmetros, mesmo nome, mesmos pixels (os 4 goldens não se mexeram).
Added #
example/— a tese do pacote numa tela só: uma semente de cor e os eixos globais alternáveis ao vivo, com o card e os botões restilizando juntos. O pacote não tinha exemplo, o que custava 10 pontos na análise do pub.dev (0/10 Package has an example) e, pior, obrigava quem chegava a montar o primeirorunApppor tentativa.
Removed #
lib/src/atoms/illustrations/app_illustration_{io,web}.dart— código morto desde que os providers de ilustração foram parafoundation/illustrations/: nada os importava e o barril não os exportava. O_iocarregava o segundodart:iodo pacote.
0.1.0 - 2026-08-10 #
Added #
AppBrandTypography— a brand now picks its display and body families. It closes the last white-label gap: color, radius, motion, style, glass and icon already belonged to the brand; typography was pinned in the package.AppIconToken— the contract of 55 icons the components use, and that every provider has to know how to serve. It is anextension typeoverString, so it is accepted anywhere aStringalready is.AppIconProvideras a theme axis, with two implementations:AppAssetIconProvider(the default, bundled SVGs, no network) andAppNetworkIconProvider(a CDN, with the base URL injected).AppIllustrationToken,AppIllustrationProviderand theAppIllustrationThemeaxis — the last corner where the package took a third-party licensed asset. The bundledemptycomes from Open Peeps under CC0, which (unlike unDraw/ManyPixels/Storyset) does not forbid redistributing it in a package.AppThemeScope(iconProvider:, illustrationProvider:)— the seam through which the APP picks the icon set. The brand cannot declare it: a large set lives in a sibling package, and the core does not depend on those.- Sibling packages
flocks_phosphor(1,512 icons × 6 weights, withPhosphorWeightas an axis) andflocks_material(the reference implementation, which proves the zero-Material thesis from outside). - The
flocksbrand — the whole palette derived from a seed throughswatchFromSeed, with Space Grotesk on display. It is the living proof of the white-label. (It used to be the only brand that worked with no network at all; sincecdnBaseUrlleftAppBrandConfig, every brand does.) neutralSwatchFromSeed— the neutral ramp needs a tone ladder of its own: the theme uses stop 300 assurfaceand 600 asoutline, and on the chromatic ladder those two sit 30 tones apart (a ratio of 2.79, below the 3.0 floor). Without it, every seed-generated brand would fail the contrast gate, in the same place.AppTextTheme.copyWithandAppBrandConfig.copyWith, which did not exist.LICENSE(MIT), thisCHANGELOGand a realREADME.tool/golden_triage.py— it builds contact sheets per family for reviewing golden failures.- The catalog freshness gate (
test/architecture/catalog_freshness_test.dart).doc/mcp/catalog.jsondrifted twice in three days because regenerating it was a manual step, and the site reads that file to publish the component count — each drift became a false number in the wild. The test runs in every PR'sflutter testand also requires that the hand-written numbers in the README match the code.
Changed #
- The catalog's prose is bilingual.
summary,description,whenToUse,whenNotToUse,props[].description,examples[].title/description,do,dontanda11ynow carryLocalizedText/LocalizedList(en+pt), anddoc/mcp/catalog.jsonemits{"en": …, "pt": …}for each of them. Both languages are required by the constructor, so no component can ship half-translated;test/architecture/catalog_language_test.dartalso fails when anenfield is left carrying Portuguese. Breaking for anyone readingAppComponentMetaor the JSON. statesandvariantsare a closed English vocabulary. They name API surface (an enum's value, an interaction state), so they are not localized; they are checked against the allow-list intest/architecture/catalog_vocabulary.dart.- The
.doc.mdfiles and this README are in English. The package is international from publication onward. The Portuguese lives on in the catalog'sptside, which is what the site publishes on its PT routes. Poppins-SemiBoldenters, and thetitle*styles now render at the weight they ask for. They had declaredw600all along and fell back to 500 because there was no 600 file on disk — the design system's "semibold" never existed. 161 goldens rebaselined.- The whole type scale is Poppins. The
display*andlabel*styles were Neutrek. Thelabel*ones move up to weight 500: Neutrek's regular read much heavier than Poppins', and keeping them at 400 erased the separation between label and body. AppIconsstores slugs, not URLs. Translating a name into an address is the brand's provider's job. Thejotapeandzxtrackbrands point at the CDN; the package's default resolves from the bundled assets.- The illustration's default accent became neutral (it was
secondary). An illustration's fill is the AREA — skin, clothing, surface — not a detail: painting it with the brand color left the whole figure monochrome in that color. It changes how the illustrations look in all 4 apps. AppIllustrationsstores slugs, not URLs, andAppIllustrationlost its own loading — the provider is what loads.AppIconbecame aStatelessWidget. The loading moved inside the provider, which also resolved a trap: the download was born ininitState, where reading the theme is unsafe — and the provider comes from the theme.swatchFromSeednow generates 11 stops (50–950), like the hand-written brands.AppBrandbuilds the registry asfinal, notconst, because theflocksbrand derives its palette at runtime.AppAuthSplitLayouttakeswebsiteUrlas a parameter. It used to readAppBrand.current.websiteUrl— the one place inlib/srcthat reached for the global brand singleton, for a string that already had sibling parameters (brandTitle,brandSubtitle,logoUrl) right next to it. A component that reads a static registry cannot be tested without it, cannot render two brands on one screen, and forces the package to have an opinion about where the data lives.nullkeeps the logo and drops only the link. Breaking only in the sense that the link now needs to be asked for.
Removed #
-
AppBrandConfigis theme configuration, and nothing else. Gone:appName,brandSubtitle,websiteUrl,cdnBaseUrl(with its seven derived asset getters andprecacheUrls),videoBasePath,playStoreUrlandpoweredByLabel. What stays isclientSlug, the palette, the six theme axes andtypography.None of the removed fields were read by
lib/src— the package carried them only so the app could read them back. The cost was never their weight: the derived getters (splashLogoUrl,otpLogoUrl,railExpandedUrland company) asserted an app architecture — that there is a splash screen, an OTP flow, a profile page, a nav rail, an auth background, and a Play Store listing. A design system that asserts that has an opinion about someone else's product, which is the same coupling this package refuses when it declines to reuseCardandScaffold.clientSlugstayed: with the asset path gone it is pure identity — the registry key, and the label that names the goldens and scopes the contrast reports. Breaking for anyone constructing anAppBrandConfig; the fix is to delete the arguments and keep that identity wherever your app already keeps its own. -
The Neutrek font. It is licensed "Personal Use Only"; in an MIT repository, using it would amount to redistribution. It was what blocked publishing the package.
Fixed #
- The OFL fonts were being redistributed without the license text, which the
SIL Open Font License requires. There is now an
OFL.txtbeside each family. AppIcons.mail,.plusand.refreshwere cited in dartdoc but did not exist. They exist now, as part of the contract.
Security #
- The package's default path no longer depends on a private CDN or on a licensed icon set (Streamline Ultimate). The default is offline and MIT.
- A brand can no longer point anywhere.
cdnBaseUrlwas first made optional, so that an adopter with nowhere to host was not pushed into copyingflocksBrand's value — which pointed at the site's own CDN, putting our infra and our logo inside their product. Removing the field settles it by absence: there is no URL to copy and no network for the package to reach.
1.0.0 #
The first consolidated version of the design system inside the monorepo: tokens,
theme, per-brand white-label, the global axes (AppStyle, AppRadiusMode,
glass, motion, transparency) and 131 components across atoms/molecules/organisms,
each with a .doc.md, a preview, a Widgetbook case and a test.