promotional bannermobile promotional banner

HelloUI

De-clutters the stock Classic Era interface and arranges the bars through Blizzard's own Edit Mode — no art overhaul, no replacement frames.
Back to Files

v0.1.0-classic

File nameHelloUI-v0.1.0-classic.zip
Uploader
DrikkDrikk
Uploaded
Jul 26, 2026
Downloads
4
Size
100.2 KB
Flavors
Classic
File ID
8510193
Type
R
Release
Supported game versions
  • 1.15.9

What's new

HelloUI

v0.1.0 (2026-07-26)

Full Changelog

  • Build the layout when a profile is created, and switch it when one is chosen
    A new profile changes the layout's NAME - "HelloUI - <profile>" - and nothing
    created a layout under that name, so Edit Mode stayed on the old profile's
    layout, IsActive answered no, and the login prompt asked for a layout the
    player had already accepted on the profile they copied from. Every session.
    Config's profile writes now call Layout:FollowProfile, which:
    - does nothing at all if HelloUI's layout was not active before the switch.
    "Asked, never applied silently" has to survive this: consent carries across
    a profile switch, it is not created by one. Read BEFORE the switch, since
    afterwards the question is about the layout we just moved to.
    - ACTIVATES an existing layout rather than re-applying it. Apply rebuilds the
    systems table from current settings, which is right when you ask for the
    layout and wrong when you merely switch to a profile that has one: Edit Mode
    saves your dragging into the layout, so a rebuild throws it away. New
    Layout:Activate only changes which layout is active.
    - builds one with Apply only when the profile has no layout yet - the case in
    the report - and defers to PLAYER_REGEN_ENABLED if that lands in combat.
    The Edit Mode nudge (show then hide the manager, which is what makes it re-read)
    is now a shared helper instead of living inside Apply, since Activate needs it
    too. IsActive takes an optional layout name for the same reason.
    352 checks. One mutation initially survived - rebuilding instead of activating -
    because the assertion held a table captured before the switch, and GetLayouts
    hands back copies, so the stale snapshot kept the marker the live layout had
    lost. It re-reads from the client now and the mutation fails.
  • Add an addon-list icon, cropped from the project art
    64x64, matching HelloWarrior's, with the TOC's IconTexture pointing at it.
    Cropped rather than scaled: HelloUI.png is a logo card - monogram badge, "Hello
    UI" wordmark, tagline, mock UI around the border - and squashing 1254px of that
    into 64 turns the wordmark into three grey smears. The HU badge is the part that
    survives at icon size, so the crop is just the badge and its ring.
    Framed by measurement rather than by eye, after two attempts that missed: a
    labelled grid over the source put the ring and its four diamond points at
    x 425-820, y 55-500, and the final 438px window at (629, 288) holds all four
    points while clearing the mock health bars on the left and the mock minimap on
    the right - both of which a looser crop dragged in as coloured specks.
    Icon.png ships in the zip; HelloUI.png still does not.
  • Release plumbing, pointed at CurseForge project 1625608
    The project ID on its own does nothing: it is read by the BigWigs packager,
    and HelloUI had no packager config and no workflow to run it. Ported from
    HelloWarrior, which is the family's working version of this.
    .pkgmeta what ships in the zip
    .github/workflows/release.yml fires on a v* tag, checks the tag against the
    TOC version, packages, uploads to CurseForge
    and cuts a GitHub Release
    .github/changelog.sh builds the release notes with commit trailers
    filtered out, which the packager cannot do
    .gitignore the generated CHANGELOG.md, .DS_Store, .claude/
    Two differences from HelloWarrior's copy, both about what this addon has that
    it does not: Tests/ and .luacheckrc are excluded from the zip - the harness
    stubs the entire client API to run under plain lua and would never be read in
    game - and HelloUI ships no IconTexture, so nothing needs keeping for the
    client.
    Also tracks HelloUI.png, the CurseForge project image, and excludes it from the
    zip for the same reason HelloWarrior excludes its own: the page needs it, the
    player does not.
    Still needed before a release can happen: the GitHub repo (HelloUI has no
    remote yet) and a CF_API_KEY secret on it. Without the key the workflow still
    cuts a GitHub Release and just skips the CurseForge upload.
  • Profiles: settings per profile, characters pick one
    Every setting lived at the top level of HelloUIDB and applied to all 47
    characters, with a sparse per-character override list beside it. That shape came
    from the DragonflightUI profile being ported - one arrangement, one diverging
    character - and it is the wrong shape for alts you play differently, because it
    makes you enumerate the exceptions.
    Settings now live in HelloUIDB.profiles[name] and HelloUICharDB stores which one
    this character uses. Get and Set resolve through the active profile; there is no
    account/override split left to reason about.
    What deliberately does NOT go in a profile: xpBarTextOriginal and proxyOriginals
    • HelloUI's memory of what Blizzard's state was before it touched anything - and
      the migration latches. Those are not preferences, and copying them per profile
      would mean a profile switch could hand the client back the wrong original.
      The Edit Mode layout follows the profile: Default keeps the bare "HelloUI",
      anything else gets "HelloUI - <profile>", always Account-typed since a profile
      is shareable and a character is not. That collapses layoutPerCharacter into the
      profile choice - one decision instead of two that could disagree.
      Migration, latched on profilesV1: the old top-level settings become Default, and
      a character with overrides gets a profile named after itself seeded with them.
      The name is not cosmetic - the old per-character layout was "HelloUI - <char>",
      so the layout that character already has is the layout it keeps.
      /hui char becomes /hui profile [use|new|delete]; the panel gains a Profile
      section with a dropdown, following HelloBuffCap's, which is the family's only
      precedent for one on this client.
      343 checks. Eight mutations tried, all caught: profiles sharing one table, a new
      profile starting blank instead of copying, settings left at the top level,
      remembered originals swept into a profile, overrides ignored by the migration, a
      deleted profile resurrected at login, the layout ignoring the profile, and
      Default being deletable.
  • File the addon under UI Overhaul
    The same category DragonflightUI sits in, which is where somebody looking for
    its replacement will look. One English line, matching every sibling - the family
    declares a bare ## Category: and none of them carry the ten localised variants
    DragonflightUI ships, and the client falls back to this line for any locale that
    has no override.
    Worth noting the tension rather than pretending it away: the Notes line still
    says "no art overhaul", and that is still true - the category describes the
    shelf, not the method.
  • Darkmode is one switch
    The desaturate toggle, the four per-area checkboxes and the tint colour are
    gone. Nobody was going to tint the minimap but not the cast bar, and the grey
    was three numbers with no way to change them short of editing saved variables.
    On desaturates and tints every texture on the allowlist; off hands all of them
    back. The tint is a constant in Darkmode.lua.
    The entries lose their area tag with the gating, and Status stops printing a
    list of areas that can no longer differ. Three more keys join the retired list
    and are deleted from saved variables - including darkmodeAreas, which the
    harness now plants as a TABLE with two areas set false, since that is the shape
    a real install carries. Darkmode must still cover everything with those on file.
  • Remove the button border feature
    Taking ActionButtonTemplate's NormalTexture from Blizzard's alpha="0.5" to full
    shipped on the reasoning that hiding the bar backdrop takes the per-slot
    recesses with it. Seen on screen, the stock half-alpha border is preferred, and
    looking at it is the only thing that settles a look.
    Nothing writes that texture now in either direction - not to set it, not to
    restore it - so the buttons carry exactly what Blizzard ships. The key joins the
    retired list and is deleted from saved variables on sight.
    The harness assertion inverts with the feature: the border alpha is set to 0.42
    first, something Blizzard never uses, then ApplyAll must leave it there. Both
    "the feature came back" and "a restore path that still writes 0.5" fail that,
    where asserting a bare 0.5 would have passed the second one.
  • Stop asking for the layout that is already active
    Layout:IsActive compared an index into the SAVED-only list GetLayouts returns
    against activeLayout, which counts the presets first. On Era that is 1 against
    3, so it answered "not active" for a layout that plainly was, and MaybeAsk
    raised the login prompt every single session.
    This is the same asymmetry that made the first version of Apply switch to a
    Blizzard preset, and Apply has done the [presets..., saved...] rebuild ever
    since. IsActive never did. Both go through one combinedLayouts helper now,
    because having the rebuild in one caller and not the other IS the failure mode.
    The harness had an assertion for this - "no prompt once the layout is already
    active" - and it passed against the broken code, because askedThisSession was
    already latched from the login prompt earlier in the run, so MaybeAsk returned
    before it ever consulted IsActive. The latch moves onto the module so a test can
    simulate the next login, IsActive is now asserted directly, and the other half
    is covered too: a player sitting on somebody else's layout must still be asked.
    All four mutations fail, including the exact shipped bug.
  • Stop the panel telling you to do what the layout already does
    Two notes in the options panel predated the layout feature and had been wrong
    ever since. The minimap section said position was Edit Mode's job and to go drag
    it there; the chat note said the same about the chat frame. Both are placed by
    HelloUI's own layout - Layout.lua writes Enum.EditModeSystem.ChatFrame at
    BOTTOMLEFT clear of the flank block, and the Minimap system with an anchor and
    its Size setting.
    The minimap section was only that note once the time-of-day checkbox was
    retired, so it goes entirely. The chat note goes too, and the layout controls
    gain a "Bar layout" header and one accurate line: what the layout covers, and
    that dragging afterwards is saved into it so re-applying is the reset. The
    button's tooltip gains the same coverage list.
    The same stale claim was in four more places - the README's opening, the panel
    subtitle, the Config comment about chat, and Minimap:Status - all of which said
    some variant of "position is Edit Mode's job and this addon stays out of it".
    The honest version is that HelloUI writes positions into an Edit Mode layout
    rather than anchoring frames behind the client's back, which is a different
    statement and the one the design actually turns on. DESIGN.md's two out-of-scope
    entries now say which half is out of scope: never SetPoint on those frames, not
    never place them.
  • Remove the friends list feature
    Class-colouring the friends list and the heart for notes containing <3 are
    gone, module and all. They were the one thing here that decorated a frame
    nobody had complained about - ported from the old profile because the code was
    short and the heart was charming, which is not a reason.
    Friends.lua is deleted, with it the FriendsFrame_UpdateFriendButton hook and
    the C_FriendList reads, so the addon no longer touches the friends list at all.
    The two keys join the retired list and are deleted from saved variables on
    sight, exactly like the six settings that became behaviour - the list now
    covers both cases, and the comment says which is which.
    The panel loses its Friends section, which leaves the right column ending at
    the minimap note. The harness loses the friends stubs, its scroll-frame model
    and four assertions, and gains the two dead keys in the retirement check plus
    their checkbox names in the no-checkbox-survives sweep. 311 checks.
  • Retire six settings into plain behaviour
    Hiding the gryphons and bar backdrop, always-visible XP and reputation text,
    hiding the time-of-day dial, class-colouring the player health bar, yielding
    the cast bar to a sibling that draws its own, and the flat cast bar are what
    HelloUI IS. They were switches because everything started as one, and a switch
    implies a decision worth making - nobody installs this addon and then turns the
    de-clutter off.
    They now run whenever the addon is enabled. enabled is the only gate above
    them, so /hui off still hands every one back, and the restore paths stay
    exercised because that is the same condition they always used.
    The keys are deleted from saved variables on sight rather than left to rot: an
    install that ever ticked one carries it forever otherwise, and a stale
    hideBarArt = false sitting in the file reads like a setting that stopped
    working rather than one that was retired. Char overrides are cleaned too.
    The panel loses six checkboxes and, with them, its "Status bars" and "Unit
    frames" sections, which had nothing else in them. It gains one line listing
    what is now unconditional - a panel that simply stopped mentioning them would
    read as features that had been dropped.
    Harness: an install with all six stored as false must come out with the keys
    gone and the behaviour still on. Every target is forced into the WRONG state by
    hand first, because asserting the right state without that passes on leftovers
    from an earlier apply - which is exactly how the bar-art check went green
    against a version still reading the retired key. 318 checks; the two mutants
    that survive are equivalent, both masked by re-assertion hooks that enforce the
    same outcome.
  • Scroll the options panel instead of overflowing the canvas
    The Settings canvas is a fixed height - about 580 units, whatever the
    resolution, since UIParent is always 768 tall - and this panel's left column
    alone is taller than that. The canvas does not clip, so the last three controls
    were drawn over the game: the layout checkboxes, all three buttons and the
    status line, floating across the action bars below the Options window.
    Rebalancing the columns would have bought exactly one release - the panel gained
    two checkboxes this afternoon - so everything moves into a scroll child inside a
    UIPanelScrollFrameTemplate. That is also what HelloHealer's settings panel
    already does, with the same comment about growing content, so it is the shape
    the family expects.
    The scroll child's height is measured from the lowest element after each
    refresh, not hard-coded: it IS the scrollbar's range, so a constant would either
    cut off the last control or scroll into empty space, and the status line grows a
    line when the character has overrides.
    The way this regresses is a new widget parented to panel rather than
    content - it escapes the scroll frame and floats over the game again. The
    harness asserts the panel has exactly one child, that the scroll child exists,
    and that a control from the far end of the layout really is inside it, so the
    first check cannot pass by the panel merely being empty. All three mutations
    fail. 311 checks.
  • Re-apply the cast bar style after Blizzard rebuilds the look
    Blanking the border art was necessary and still not sufficient. CastingBarMixin
    :SetLook("CLASSIC") rebuilds the bar's appearance from scratch:
    self.Border:SetTexture("Interface\CastingBar\UI-CastingBar-Border") :1015
    self.Text:ClearAllPoints(); self.Text:SetPoint("TOP", 0, 5) :1028
    self.Text:SetFontObject("GameFontHighlight") :1029
    and PlayerFrame_DetachCastBar calls it from EditModeCastBarSystemMixin
    :ApplySystemAnchor, which runs on every Edit Mode layout update - at login,
    after our styling pass, and on every close of Edit Mode.
    So the border came back, the spell name went back to the full-size font, and
    its box was re-anchored 5 units above the bar's top, which is where the classic
    border art has room for it and a flat bar does not. That last part is the
    "transparent top half": the name was drawn above the bar with nothing behind it.
    Measured off the screenshots rather than guessed: the name's ink is 15px tall
    starting 1px above the bar, while the countdown - which SetLook does not touch -
    is 9px inside it. A partial undo, which is why it read as a styled bar whose
    border had mysteriously returned rather than as Blizzard's own bar.
    Fixed with a hook on the instance, the same shape as the UpdateBarFillTexture
    one next to it. The harness now models SetLook faithfully and asserts all three
    properties survive it; removing the hook fails three checks, and ignoring the
    setting inside it fails a fourth. 307 checks.
  • Blank the cast bar's border art instead of hiding it
    Hiding it worked until a cast COMPLETED, and then the outline flashed back for
    about a second. Blizzard's own finish path is why:
    self.Flash:SetAlpha(0.0); self.Flash:Show(); CastingBarFrame.lua:453, 571
    if self.FlashAnim then self.FlashAnim:Play() end :743
    <AnimationGroup parentKey="FlashAnim" setToFinalAlpha="true">
    <Alpha childKey="Flash" fromAlpha="0" toAlpha="1" duration="0.08"/>
    UI-CastingBar-Flash is the same 256x64 border shape as the border itself, so a
    completed cast re-Shows it, animates it to full alpha, and setToFinalAlpha
    leaves it there through the fade-out. Hide() loses to the Show; SetAlpha(0)
    loses to the animation, which owns that property outright. This is the Show/Hide
    fight Bars.lua already documents, one frame over.
    A texture with no file draws nothing regardless of who shows it or what its
    alpha is, so all three regions are blanked with SetTexture(nil) and their files
    remembered for the restore. Blizzard's Show, its SetAlpha and its animation all
    still run, and all still paint nothing. Shown-state is no longer recorded or
    restored, because HelloUI no longer touches it.
    The harness now replays that exact sequence - Show plus alpha to 1 - and asserts
    the art stays blank, so the reported bug has a test. Going back to Hide() fails
    five checks. 302 checks, 5 of 5 mutations caught.
  • Stop the spell name climbing out of the cast bar
    Blizzard declares PlayerCastingBarFrame's Text as GameFontHighlight in a region
    sized 185x16, inside a bar that is 13 tall. Centred in 13px, a 16px box stands
    about 1.5px proud top and bottom - stock never showed it because the 256x64
    border art drew over the overspill, and hiding that border is exactly what this
    module does.
    A smaller font rather than a clamped height: a FontString does not clip its ink
    to its rect, so shrinking the box moves the problem instead of fixing it. Word
    wrap off with it, since a long enough name takes a second line and leaves the
    bar by a whole line height instead of a pixel and a half. Both are restored
    when the style is switched off, the font from what the frame actually had
    rather than from a constant.
    HelloWarrior's bar was never affected - it is 13 tall with a small font - and it
    now uses the same 195x13 and the same font object, so the two are one bar.
    299 checks; the missing-font, missing-wrap and unrestored-font mutations all
    fail.
  • Restyle Blizzard's cast bar to match HelloWarrior's
    Two facts made this art removal rather than a replacement bar. Blizzard's
    classic-style cast bar ALREADY draws the same fill texture HelloWarrior's does
    • CastingBarMixin:UpdateBarFillTexture sets Interface\TargetingFrame\UI-StatusBar
      on that path - so the whole visible difference is the 256x64 border wrapped
      around a 195x13 bar, a centred spell name, and no countdown. And there is no
      CastTimeText on this build at all: UpdateCastTimeTextShown opens with
      if not self.CastTimeText then return end and the Classic template declares
      none, so the timer has to be ours.
      So: hide the border and the flash that lights the same outline, a flat backdrop
      behind the fill, the name to the left, a countdown on the right fed from the
      mixin's own value/maxValue so the digits cannot disagree with the fill. No
      shipped textures - the backdrop is a solid colour and the rest is Blizzard's.
      The colour is the one thing needing re-assertion: UpdateBarFillTexture
      re-applies a per-bar-type colour on every cast and state change, so it is
      hooked on the INSTANCE, since Mixin() copies the method onto the frame and
      hooking the mixin table would reach nothing.
      Two harness fidelity bugs fixed on the way, both of which hid real behaviour:
      SetPoint REPLACED the anchor list instead of accumulating, so a frame anchored
      LEFT then RIGHT - how the spell name is stretched - reported only the RIGHT
      point, and GetPoint(1) is meant to be the first anchor set. And SetAllPoints /
      SetColorTexture were no-ops, so "the backdrop exists" would have passed for a
      texture covering nothing and painted with nothing.
      Also fixed a real bug luacheck caught in the restore path: f.Text and f.Text:GetPoint(1) truncates a multiple return to one value, so it recorded
      the point and silently dropped all four offsets.
      296 checks, 11 of 11 mutations caught.
  • Yield the cast bar when a sibling is drawing its own
    HelloUI's layout parks PlayerCastingBarFrame in the strip HelloWarrior's
    cluster occupies, so on a Warrior they draw through each other. HelloWarrior
    now draws its own bar at the top of its cluster; this switches Blizzard's off
    while it is there.
    The mechanism is Blizzard's own, not a workaround. ShouldShowCastBar is
    self.showCastbar and self.unit ~= nil, and every path that would show the bar
    goes through it, so SetAndUpdateShowCastbar(false) is a supported flag rather
    than a Hide() we would have to defend against Blizzard re-showing. It is the
    same call the client makes in OverlayPlayerCastingBarMixin, under the comment
    "Disable real Player Cast Bar". Nothing re-asserts it, so neither do we.
    Detection is by FRAME, not by addon. IsAddOnLoaded("HelloWarrior") is true on a
    Priest with it installed, where it builds nothing at all - taking that Priest's
    cast bar away would be a bug, and it is the mutation the harness now catches.
    The real question is whether a sibling is drawing a cast bar right now, and the
    honest test is its frame existing and its cluster being shown. Two named
    globals, read-only; HelloWarrior does not know this file exists.
    Known edge, stated rather than papered over: /hw bars off hides the cluster
    and Blizzard's bar returns on the next apply pass rather than instantly. The
    alternative is polling a sibling's visibility every frame.
    Harness: CastBar.lua was not in the loader's file list, so the whole module
    loaded in game and was never exercised offline - green suite, untested feature.
    There is now a check that every .lua in the TOC is in that list, and dropping
    the entry fails it. 276 checks, 7 of 7 mutations caught.
  • Pack the tracking button against the eye, and grey the minimap's gold
    TWO CHANGES, both measured off a screenshot rather than reasoned about, since
    reasoning about them is what produced the numbers being replaced.
    The angle: 30 degrees came from the 33x33 FRAMES, and the frames are much
    bigger than the art they carry. The visible gold ring is about 26.5 units
    across, so frame-tangent still left a 15.7-unit hole - measured at 21.87px on
    screen, twelve times the median gap between the buttons already packed on this
    rim. The right target is the rim's own pitch: all twelve buttons ride one
    circle (fit rms 0.30px) and the packed pairs sit a median 24.9 units apart.
    At r=75 that is 19 degrees, chord 24.8.
    Changing the default cannot do that on its own - applyDefaults only fills
    missing keys and every install that has ever logged in has trackingAngle = 30
    on file. So, a latched migration, value-guarded this time: a stored 30 moves to
    19, a deliberate 45 does not. ResetAccount re-latches, which is the bug the
    barsBaseV2 comment already records. The honest hole is in the comment: someone
    who deliberately typed 30 is indistinguishable from someone who never touched
    it, and gets moved.
    The gold: darkmode's minimap area covered the map's own ring, the header and
    the zoom buttons - and none of the gold RINGS around the buttons sitting on
    top of them. Eight entries added, every one a name Blizzard's own XML gives it:
    the four that share MiniMap-TrackingBorder (tracking, LFG, mail, battlefield),
    the minimise button in normal and pushed state, and the compass pair. Still no
    enumeration, so the four Hello*MinimapButtons cannot be caught by construction.
    MiniMapWorldBorder is deliberately absent - MiniMap_ShouldShowWorldMapButton()
    is return false on Era, so it would only inflate the found count. The icons
    inside the rings stay coloured: tint bezels, never payloads.
    Harness: two of the rules this module is built on were unenforceable. A
    children sweep of Minimap passed the entire suite because Frame had no
    GetRegions, and the live-state check compared only the desaturation flag, which
    SetDesaturated makes idempotent. Both are real now - a sibling button with an
    anonymous ring, a live-state icon with its own tint - and both mutations fail.
    264 checks, 10 of 10 mutations caught, including an angle clamp that would have
    silently turned a deliberate 45 into 20.
  • Match the tracking ring to the LFG eye - Blizzard declares it 64, not 52
    Third defect in the same 33-line file. Both buttons are 33x33 frames and both
    rings are the same texture, Interface\Minimap\MiniMap-TrackingBorder, with no
    TexCoords or colour on either - so the declared size is the entire difference:
    LFGMinimapFrameBorder 52x52 at TOPLEFT (1,-1)
    MiniMapMailBorder 52x52
    MiniMapBattlefieldBorder 52x52
    MiniMapWorldBorder 52x52
    MiniMapTrackingBorder 64x64 <- the vanilla file again
    64/52 = 1.2308, and a sub-pixel circle fit on the screenshot measured the two
    rings at 42.91px and 34.98px, ratio 1.227 +/- 0.008. At 64 the ring overflows
    its own 33x33 frame by 4 units; at 52 it fits, like every other button's.
    The anchor moves with the size, which is not optional: the ring art is not
    centred in its file, so resizing to 52 while leaving the border flush at (0,0)
    slides the ring 3.47 units off the icon still sitting at Blizzard's (2,-2).
    Copying the eye's (1,-1) verbatim sidesteps needing to know that fraction, and
    the icon's own offset goes to (0,0) since it only ever cancelled the 64 ring.
    SetSize on the regions, never SetScale on the frame: offsets live in the
    anchored frame's own space, so SetScale(52/64) would render the 75-unit polar
    vector at 61 against a rim at 70 and put the button back on the map - correct
    art, broken placement, and the existing radius assertions could not see it
    because they read the raw offsets. That mutant now fails 18 checks.
    The target is read live off the eye's own ring and converted through both
    effective scales, because a texture's GetWidth is in its own frame's space and
    a button pack that scales the eye would otherwise leave the two rings equal on
    paper and 25% apart on screen.
    Switching the feature off now hands back both sizes AND both region anchors;
    sizes alone would leave Blizzard's 64 ring around an icon centred for a 52 one.
    Parent, shown state and the polar anchor still need the /reload that /hui off
    already tells you about.
    Harness: the two tracking regions and the eye's ring did not exist as stubs at
    all, so every assertion here would have passed vacuously. 234 checks, and all
    17 mutations tried are caught - including the two that survived the first pass,
    a scale-blind copy and a foreign MiniMapTrackingFrame capturing the anchors.
  • Put the tracking button on the minimap's rim, not on the map
    Anchoring it below the LFG eye translated a point on a circle, which cuts
    the chord: the eye rides the rim 75 units from the map's centre, and 35
    units straight down leaves 57 on a map whose rim is at 70. The icon was
    therefore drawn on the map itself, next to the eye but inside the artwork.
    Rotate instead of translate - take the eye's own offset from the centre
    and turn it 30 degrees, which preserves the radius by construction. The
    angle is a setting, nudgeable with /hui tracking <degrees>, because the
    rim is shared with every other addon's minimap button and nothing in here
    can see which slots those have taken.
    Also: /hui minimapprobe now prints the three radii, which is the one
    line that would have made the mistake obvious, and the harness gained
    real frame rects - GetLeft answered 0 for everything, so every frame
    shared a centre and any placement would have passed.
  • Show the tracking button - Blizzard never does at login
    The probe settled it in one line:
    tracking parent=MinimapBackdrop shown=false alpha=1.00 level=5 at 1219,683
    lfg parent=MinimapBackdrop shown=true alpha=1.00 level=6 at 1219,718
    GetTrackingTexture() = 135725
    Correct parent, correct position directly under the LFG eye, right icon, full
    alpha, and hidden. The positioning was right two attempts ago; the frame was
    simply never shown, and every hypothesis I had been testing - wrong parent,
    behind the map, zero alpha - was wrong.
    MinimapTrackingSimpleMixin only shows it from MINIMAP_UPDATE_TRACKING, which
    fires when tracking CHANGES. Log in with tracking already active and the frame
    stays at its XML hidden="true" forever. That is a second Era bug in the same
    frame, on top of the missing parent attribute, and both hide in the shipped
    client because only classes with a tracking ability ever see it.
    The fix is the three lines Blizzard's own handler runs, also run once at apply,
    and mirrored on MINIMAP_UPDATE_TRACKING so it stays in step afterwards. Hidden
    again when nothing is tracked, matching Blizzard.
    Clock offset defaults to the 1, -1 dialled in live, replacing Blizzard's 3, 1.5
    • which is what the nudge command was for, and it took one round trip instead
      of the three my guessing had used.
      191 checks. Mutation-tested by positioning the frame without showing it, which
      reproduces exactly what was on screen.
  • Make the clock offset nudgeable; probe the tracking button's real state
    Two aesthetic details I have now guessed at three times between them, so this
    stops guessing and gets data instead.
    THE CLOCK DIGITS. Blizzard's x=3 is an attempt at optically centring them in
    box art that is not symmetric - the button's own HitRectInsets are 8 left and 5
    right, implying the visible box centre is about 1.5 off, not 3. Whether 3 is
    right is not something the source can settle and certainly not something I can
    read off a JPEG, so the offset is a setting nudgeable live with
    /hui clock <x> <y>, defaulting to Blizzard's own with the half-pixel y
    rounded. Two seconds per attempt instead of a round trip each.
    THE TRACKING BUTTON is invisible for a reason none of my hypotheses can
    distinguish - wrong parent, behind the map, never shown, zero alpha - so
    /hui minimapprobe now reports parent, shown, alpha, frame level and rect for
    the button, its icon, LFG, the backdrop, the minimap and the clock, plus what
    GetTrackingTexture() actually returns. That last one separates "HelloUI moved
    it somewhere invisible" from "Blizzard never showed it".
    No behaviour change to the tracking fix itself pending that output.
  • Anchor tracking under the LFG icon; fix the clock by compensating its scale
    THE TRACKING ICON VANISHED because of a guard I added in the same commit that
    moved it - "if it already has a parent, somebody else owns it, leave it alone".
    MiniMapTracking's parent after a reload is whatever the parentless XML leaves
    it with, the guard read that as somebody else's claim, and skipped the fix
    entirely. Speculative defensiveness for a case never observed, breaking the
    case that was. The guard is gone, and the harness now hands the frame a parent
    up front so re-adding it fails the tests.
    It is now anchored TOP to LFGMinimapFrame's BOTTOM rather than to fixed
    coordinates, so it sits directly under the eye on the ring's edge and follows
    it wherever it moves. A hidden frame still reports its position, so this works
    before either icon is shown. Blizzard's own backdrop position stays as the
    fallback for when LFG is absent.
    THE CLOCK, properly this time. The previous attempt rounded Blizzard's
    half-pixel anchor, which could not work: the minimap is scaled to 110%, so an
    offset of 2 lands at 2.2 screen pixels and no integer helps. The cause is the
    scale, so the button's own scale is compensated to bring its EFFECTIVE scale
    back to UIParent's - the digits then render at native size on the grid, and the
    clock stays its original size while the map around it is bigger. Only once
    that holds is rounding the anchor meaningful, so it is done afterwards rather
    than instead. Re-applied on UI_SCALE_CHANGED and DISPLAY_SIZE_CHANGED, since
    the compensation depends on a scale that can change.
    185 checks. Both mutation-tested: re-adding the guard, and rounding the anchor
    without compensating the scale.
  • Default the clock text rounding off - it did not help
    Reported as making the clock worse, so it is off.
    The reasoning was that Blizzard anchors the ticker at a half pixel and text off
    the grid renders smeared. That much is true, but the fix does not follow: the
    minimap is scaled to 110%, so an offset of 2 renders at 2.2 screen pixels and
    no integer offset lands on the grid while that scale is in force. I moved the
    text half a pixel for no benefit. Blizzard's 1.5 may also be optical rather
    than accidental, in which case rounding it is actively wrong.
    Kept behind a setting rather than deleted, because at 100% the arithmetic does
    hold and it may be worth something there. The test covers both states.
    The likely real cause of the soft clock is that 110% scale, which is a plain
    trade rather than a bug: minimapSize 5 is crisp at the original size, 6 is
    bigger and slightly soft, and 200% is the next whole-number scale and far too
    large. Documented at the setting so the choice is visible.
  • Fix the clock's half-pixel text, and put tracking where Blizzard puts it
    THE CLOCK. Blizzard anchors the ticker at a half pixel -
    <Anchor point="CENTER" x="3" y="1.5"/> in Blizzard_TimeManager - and a font
    string off the pixel grid renders smeared, which is what reads as misaligned.
    Only the y is rounded; the x=3 is deliberate, centring the digits inside dial
    art that is not symmetric. Blizzard_TimeManager is LoadOnDemand, so it is
    re-applied if the addon arrives after us.
    TRACKING vs LFG. Yesterday's fix used DragonflightUI's offsets, which put the
    button in the ring's upper left - straight on top of LFGMinimapFrame at
    MinimapBackdrop TOPLEFT (25, -28). Blizzard's own vanilla anchor, TOPLEFT
    (11, -26), collides just as badly: 14px apart with 33px icons.
    The answer was already in the tree. The same frame name is declared in
    MinimapTracking_Dropdown.xml - the variant every non-vanilla flavour loads -
    as parent="MinimapBackdrop" at TOPLEFT (5, -64), a full row below LFG. So that
    is where Blizzard puts this button when the parent attribute has not gone
    missing, and it is the right answer rather than merely a different one.
    Also added: a guard so a tracking button somebody else has already parented is
    left alone, and the minimap size is now a config value rather than a constant,
    because a non-integer scale puts the whole minimap subtree on fractional
    pixels - if the clock text still looks soft at 110%, 100% is the value that
    does not, and the comment says so.
    185 checks, all three mutation-tested including the naive (11,-26) position.
    SetParent was stubbed as a no-op in the harness, which let the reparent "pass"
    while doing nothing - the same gap SetWidth had. It is real now.
  • Chat above bar 5, bigger minimap, tracking button back on the map, no paging arrows
    Four fixes from an Edit Mode screenshot, three through the layout and one a
    genuine Blizzard bug.
    CHAT UNDER BAR 5. Switching bar 5 on put a 4x3 block exactly where Blizzard
    parks the chat frame, so they overlapped. Chat is now positioned in the layout,
    lifted above the block. This is not a reversal of the earlier refusal to pin
    the chat frame: that was about fighting Edit Mode with SetPoint on a frame Edit
    Mode owns and silently re-anchors. Writing it into the layout is the sanctioned
    route, the same mechanism every other frame here uses, and it stays draggable
    afterwards.
    MINIMAP one slider step larger - raw 5 is 100%, raw 6 is 110%.
    THE TRACKING BUTTON is broken in Blizzard's own XML on Era.
    Blizzard_Minimap/Classic/MinimapTracking_Simple.xml declares MiniMapTracking
    with NO parent attribute and an unqualified TOPLEFT anchor, and the
    accompanying Lua never reparents it - so it lands in the top-left corner of the
    screen beside the player frame instead of on the minimap. The non-vanilla
    variant of the same file has parent="MinimapBackdrop"; the vanilla one lost it.
    Earlier research flagged this as uncertain because only classes with an active
    tracking ability ever see it; the screenshot confirms it. Reparented onto the
    minimap at DragonflightUI's offsets.
    BAR 1'S PAGING ARROWS hidden through Blizzard's own HideBarScrolling, written
    only on the main bar since that is the only one carrying the setting.
    175 checks, each of the three layout changes mutation-tested. One existing
    assertion used the minimap as its "untouched system" control and had to move to
    a system HelloUI genuinely never positions, now that the minimap is one it does.
  • Position bars 6-8 even though they ship switched off
    Answering "why are 6, 7 and 8 off?" turned up a latent bug behind the answer.
    They are off because DragonflightUI's own defaults have activate = false for
    all three, and the shipped default is meant to be its base action bar UI. Not
    because Classic Era cannot use them - MultiBar5/6/7 are real on this build,
    with action pages 13/14/15, their own MULTIACTIONBAR5-7 bindings and Blizzard's
    own visibility checkboxes.
    But the layout was not positioning them, and Blizzard's preset parks those
    three in the MIDDLE OF THE SCREEN - TOP relative to UIParent CENTER at 0, -50
    and -100. So switching one on would have dropped a row of buttons across the
    player's view, with nothing to suggest HelloUI had anything to do with it. They
    now continue the stack upward above the stance/pet row, on the same argument
    already applied to MultiBarLeft: a bar that is off should still land correctly
    the moment it is turned on.
    170 checks. Mutation-tested by removing the three entries again.
  • Scale the status bar's border art, not just its frame
    "In Edit Mode it looks right, but the graphics are wrong" was the clue that
    identified this precisely: the frame was the correct width and the artwork was
    not.
    The container's border is a fixed chain of textures anchored left-to-right
    from its left edge - standalone is 16 + 240 + 256 + 256 + 256 = 1024, the
    main-menu set is 4 x 256 - and none of them follow the frame. So resizing the
    container moved Edit Mode's selection box and left the bar drawn on screen at
    its original 1024, which is why every measurement I took said the geometry was
    correct while the screen disagreed. The probe could not see it either: it
    reports frame rects, and the frame was right.
    Every segment is scaled by the same factor rather than stretching the middle
    ones, because each carries its own TexCoords into the source image and
    stretching them unevenly distorts them against each other. The segments are
    named explicitly rather than walked, because the 9x9 end caps live in the same
    parentArray and are anchored to the first segment - they keep their natural
    size.
    Originals are recorded, so /hui off restores the art along with the width.
    164 checks. Mutation-tested by resizing the frame without the art, which
    reproduces the reported symptom exactly.
  • Make empty action slots a setting
    The probe confirms the geometry is now exact: stack 478..886 and status bar
    478..886 in screen space, zero overhang either side; three rows 38 apart at 35
    tall; both flanks 150x112, that is 4x3, sitting 20 and 21px clear of the stack.
    Widths, pitch, alignment and shape are all where they should be.
    What is left is that turning AlwaysShowButtons on - done so the flanking bars
    would read as 4x3 blocks rather than a couple of stray buttons - means a bar
    you have not filled now shows a full grid of empty squares. That is arguably
    worse clutter than the problem it fixed, and which way it should go depends
    entirely on whether those bars are used, so it is a setting rather than a
    decision made on the player's behalf.
    Default stays on, matching DragonflightUI's alwaysShow and the reference
    screenshot. It lives in the Edit Mode layout, so it needs /hui layout to take
    effect, and the tooltip says so.
    161 checks, mutation-tested in both directions.
  • Derive the XP bar width from the real action bar, through effective scales
    The bar overshot the stack on the right because 454 was being set in the
    container's own coordinate space, not screen pixels.
    StatusTrackingManagerMixin:UpdateBarVisuals calls SetScale(self.ClassicScale)
    on the manager, so a container at scale 1.4 renders a "454" width as 636 on
    screen - wider than the stack by exactly the overhang reported.
    The width now comes from MainActionBar:GetWidth() at apply time, converted
    through both frames' effective scales, so it matches whatever the stack
    actually measures whatever either scale happens to be. The configured 454
    remains only as a fallback for when the main bar cannot be read.
    /hui layout probe now prints the two spans side by side in screen space, with
    the left and right overhang called out, so any residual mismatch is a number
    rather than an impression.
    159 checks. Mutation-tested by ignoring the scale conversion, which is now
    caught rather than shipped. The mock's action bars had the default 100px width,
    which would have let a wrong derivation look right - they are 454x36 now.
  • Set the XP/reputation bar width directly instead of scaling it
    Scaling was the wrong lever and produced both complaints at once. Edit Mode's
    "Size" for these bars is SetScale(value / 100), so narrowing the bar squashed
    its height - hence "too thin" - and the slider floors at 50%, which on a
    1024px container is 512 against a 454px stack, hence "still too wide". No
    setting in Edit Mode can express what was wanted, so Size goes back to 100%
    and the width is set directly.
    Three frames per container, because they do not follow each other:
    StatusTrackingBarTemplate's inner StatusBar carries an explicit
    <Size x="1024"> and is anchored only at RIGHT, so it does not track its
    parent's width, and StatusTrackingBarContainerMixin:InitializeBars stamps that
    size in again at creation from the container's width. Missing any one of the
    three leaves a bar that is visibly the wrong width.
    Re-asserted after UpdateBarVisuals, which Blizzard calls on anchor changes and
    from UIParent_ManageFramePositions. The hook is on the manager INSTANCE, not
    StatusTrackingManagerMixin - Mixin() copies functions onto the frame at
    creation, so hooking the mixin table would reach nothing.
    Original widths are recorded so /hui off restores them, same contract as every
    other thing this addon changes.
    156 checks. Mutation-tested by skipping the inner StatusBar and by dropping the
    re-assert hook. The harness had SetWidth stubbed as a no-op, which would have
    let this pass while doing nothing - it is real now.
  • Position the cast bar and narrow the XP/reputation bars
    CAST BAR. Blizzard's preset parks it dead centre of the screen; it now sits
    just above the action bars at DragonflightUI's own default of CENTER/BOTTOM
    +245. LockToPlayerFrame is written explicitly rather than inherited, because
    if it were ever on the cast bar would attach itself to the player frame and
    ignore the anchor entirely - the same reasoning as stating orientation on the
    side bars.
    XP/REPUTATION WIDTH, and an honest limit. The status tracking bar's "Size" is
    not a width, it is a scale: Edit Mode does SetScale(value / 100) on a container
    that is 1024 wide. The slider runs 50 to 130 in steps of five, so the narrowest
    it offers is 50%, which is 512px against the 454px stack. Closer than the
    full-width 1024 it shipped at by a long way, but not an exact match, and it
    cannot be made one through this setting - 454 would need 44% and the slider
    does not go there. Scaling also shrinks the height, which happens to be right:
    the bar ends up slim rather than merely short, matching the reference.
    146 checks, both mutation-tested - reverting the cast bar to the preset anchor
    and the status bars to full width each fail their assertions.
  • Stack the centre bars flush; make the side blocks 4x3
    Both reported problems came from the same class of mistake - assuming a value
    instead of reading it.
    THE GAP BETWEEN THE ROWS. ActionButtonTemplate is 36x36, so 100% icon size is a
    36px button, not the 45 I had assumed when picking a 48px row pitch. That left
    a 12px gap. The pitch is now derived from the template - button plus the 2px
    padding, so 38 - which is the same pitch as the gap between buttons within a
    bar, and the three bars read as one continuous grid instead of three bars with
    air between them. FLANK_X follows from the same arithmetic: half the
    twelve-wide stack (227) plus half a four-wide block (75) plus a 20px gap.
    THE SIDE BLOCKS WERE 3x4. Blizzard's preset sets both MultiBarRight and
    MultiBarLeft to Orientation = Vertical, and on a vertical bar NumRows counts
    COLUMNS - so rows=3 produced three columns of four rather than four columns of
    three. Every positioned bar now states its orientation explicitly instead of
    inheriting whatever the preset happened to have, which is the more defensible
    rule anyway: the layout should not depend on preset values it never set.
    139 checks. The row-pitch assertion now expresses the actual intent - never
    overlapping a 36px button, and never exceeding button+padding, so a future
    change cannot silently reintroduce either the overlap or the gap. Orientation
    is mutation-tested by letting the side bars inherit the preset again.
  • Show empty button slots, and add a geometry probe
    The flanking bars were rendering as one or two stray buttons rather than the
    4x3 blocks the arrangement assumes. Blizzard's preset has AlwaysShowButtons
    off, so empty slots are hidden and a mostly-empty bar looks broken rather than
    empty. DragonflightUI's own default is alwaysShow = true, so that is now set on
    every bar the layout positions.
    /hui layout probe prints what the frames actually report - left, bottom, width,
    height and shown, for every bar plus the status tracking container, the micro
    menu and the bags, against the screen size. Everything in this module has been
    written blind: the harness can prove the layout table is correct but has
    nothing to say about where a frame lands, and reading pixel positions off a
    JPEG is guesswork that has now been wrong several times in a row. This turns
    the remaining questions into arithmetic.
    The doubled screenshot text is confirmed not to be HelloUI - it survives
    /hui off, UIErrorsFrame is not an Edit Mode system, and no installed addon
    registers the screenshot events.
  • Migrate the saved bar set; bigger buttons, tighter rows, park the micro menu
    WHY BARS 5-8 WERE ALL DARK. Changing a default cannot change saved state.
    applyDefaults only ever fills in MISSING keys, so an install that had already
    saved barsOff = { bar5 = true } kept it, and the new default helpfully added
    6, 7 and 8 on top - four dark bars instead of three lit ones. Defaults cannot
    migrate; migrations have to. There is now a one-time, latched migration that
    replaces the profile-derived set with DragonflightUI's base one, and a test
    that runs it against exactly that old database.
    While fixing it: /hui reset wiped the migration latch along with everything
    else, so the migration would re-run at the next login and undo any bar the
    player had changed since. Reset already installs the base set, so it now marks
    the migration done.
    BUTTONS WERE TOO SMALL AND THE ROWS TOO AIRY. Both came from taking
    DragonflightUI's buttonScale of 0.8 literally: that scales ITS buttons, and
    matching the number rather than the result gave 36px icons with a 4px gap that
    read as loose. Icons are 100% now and the row pitch is 48 - the button plus a
    hair - which is simultaneously bigger buttons and less air. FLANK_X follows to
    400, still derived rather than eyeballed: half the stack (282) plus half a
    block (94) plus a gap.
    THE MICRO MENU AND BAGS were sitting on top of the reputation bar. Both are
    Edit Mode systems, and Blizzard's default puts them along the bottom
    centre-right - which is precisely where the rep bar went once it was pinned
    under the stack. Parked in the bottom-right corner, bags below the menu.
    Not touched: the doubled "screenshot captured" text. UIErrorsFrame is not an
    Edit Mode system and nothing here goes near it, so this is very unlikely to be
    HelloUI - see the note to the player for how to confirm in one command.
    133 checks. Mutation-tested against removing the migration, against reset
    forgetting to re-latch, and the old-database case is now a first-class test
    rather than something the fresh-install harness could never reach.
  • Default to DragonflightUI's base bar set; make barsOff authoritative
    Two changes, and the second is a deliberate reversal.
    THE DEFAULT IS NOW THE BASE UI, not the old profile's deviations:
    DragonflightUI's own defaults are bars 1-5 shown and 6-8 off (activate=false
    for those three), so that is what ships. The previous default had bar 5 off
    purely because the saved profile happened to say so, which was the wrong
    baseline to have generalised from.
    barsOff IS NOW AUTHORITATIVE. Anything in it is hidden, anything not in it is
    shown. The earlier rule would only ever turn a bar OFF, added in response to a
    review finding that force-enabling silently overwrote a setting HelloUI does
    not own. That rule cannot express "the base UI has bar 5 up": it left bar 5
    dark forever because the player's stored Blizzard setting had it off, and no
    amount of changing the default would have fixed it.
    What made the old behaviour a bug was that it was silent and unasked-for. What
    makes this acceptable is different on every count: it is the documented job of
    the feature, every bar is individually switchable, and the pre-existing value
    is remembered in saved variables so /hui off hands it straight back. That
    restore is the safeguard the reversal rests on, so it is the thing the new
    tests actually check - and it is mutation-tested by dropping the remember step,
    which fails two assertions.
    127 checks.
  • Report which layout systems were positioned, instead of skipping in silence
    The reputation bar is still not under the stack, and the reason the cause was
    not obvious is a bad guard of mine: if the status tracking bar enums are absent
    the geometry entries are dropped without a word, so the layout reports success
    while two of nine systems were never touched.
    /hui layout status now names them: how many of the wanted systems were found
    and positioned, which ones were not found, and separately whether the status
    tracking enums exist on this client at all.
    No behaviour change to the layout itself. This is to stop guessing at pixel
    geometry from screenshots.
  • Fix the layout activating a Blizzard preset; match the reference screenshot
    THE BUG. Accepting the prompt switched to Blizzard's Modern preset instead of
    the HelloUI layout. C_EditMode.GetLayouts() returns only the SAVED layouts, but
    activeLayout - and every index SaveLayouts cares about - counts the PRESET
    layouts first. So an index computed against the saved-only list is short by the
    number of presets and lands on one of them. The list has to be rebuilt as
    [presets..., saved...] before any index means anything, and that combined list
    is what SaveLayouts wants handed back.
    Three more things were missing from the same sequence: new layouts belong after
    the last entry of their own type rather than on the end, SetActiveLayout is the
    activation path rather than OnLayoutAdded, and saving does not apply - opening
    and immediately closing EditModeManagerFrame is what makes Edit Mode re-read.
    Also gated on EditModeManagerFrame.accountSettings, since writing before Edit
    Mode has its data produces a layout that saves and then evaporates.
    None of that is guessable from the API documentation. It is lifted from
    LibEditModeOverride (plusmouse, MIT), the known-working implementation of this,
    which DragonflightUI vendors - reimplemented in about forty lines rather than
    vendored, under the no-libraries rule, and credited in the source.
    MATCHING THE REFERENCE. Measured off DragonflightUI's own default screenshot:
    row pitch tightened from 44 to 40 so the rows sit snug the way the screenshot
    shows while still clearing a 36px icon, stance and pet centred above the stack
    rather than offset apart, and - the visible gap - the XP and reputation bars
    are now positioned too. Blizzard's preset anchors those to
    StatusTrackingBarManager, which strands them among the button rows once the bar
    art they were sitting on is hidden; they are pinned under the stack instead.
    Only action bars get NumRows/IconSize/IconPadding, since the status bars carry
    a different setting enum.
    MultiBarLeft stays switched off - the old profile had it off, and the layout
    positions it correctly for whenever it is turned on.
    124 checks. The harness stub now models the asymmetry properly - it derives the
    active layout name from presets-then-saved, as the client does - so the
    mutation reproduces the reported symptom exactly: "got Modern, want HelloUI".
    The insert position is clamped so a miscount degrades to a wrong slot rather
    than a hard throw from table.insert.
  • Ask before applying the layout; fix the overlap; visible button borders
    Four things, one of which was a real bug the harness caught.
    THE LAYOUT OVERLAPPED. Chaining bar-to-bar was wrong: an Edit Mode action bar
    frame is shorter than the buttons it contains, so a 2px gap measured from the
    frame's top edge lands inside the row above, and the screenshot showed rows
    ~26px apart with 36px icons. Everything is UIParent-relative at explicit
    offsets now, with a deliberately generous 44px step - a gap slightly too big
    reads as a choice, one pixel too small reads as a bug. The flanking 3-row
    blocks sit at +/-330 from centre. Those numbers are estimates and are meant to
    be nudged in Edit Mode; that is the point of writing a layout rather than
    enforcing one.
    LAYOUT WAS IN ns.MODULES, which meant ApplyAll called Layout:Apply on every
    PLAYER_ENTERING_WORLD and every options change - rewriting the Edit Mode layout
    constantly and undoing any bar the player had dragged. That is precisely the
    behaviour the rest of this design exists to prevent, and it would have been
    invisible in play until someone moved a bar and watched it snap back. Layout is
    now wired by hand for Init and Status only, with the reasoning in a comment so
    nobody adds it back.
    ASK, DON'T DECIDE. The silent one-time auto-apply is gone, replaced by a login
    prompt with Use it / Not now / Never. It asks once per session and only when
    the layout is not already active, so accepting retires the question for good.
    The applied-once latch went with it - the prompt subsumes what it was for.
    BUTTON BORDERS. Hiding the bar backdrop took the per-slot recesses with it, and
    ActionButtonTemplate's own NormalTexture ships at alpha 0.5 because it was only
    ever meant to sit on top of that backdrop - so the buttons read as invisible.
    Full alpha, using Blizzard's own UI-Quickslot2. Deliberately not
    DragonflightUI's answer, which replaced NormalTexture with its own atlas slice
    and would mean shipping textures.
    Also: opting a character out of the shared layout is now a per-character
    override rather than an account setting, which is what was asked for and what
    Config's override machinery was built for. Everyone shares one layout until a
    particular character ticks out.
    114 checks. Mutation-tested against re-adding Layout to ApplyAll, against
    leaving the border at 0.5, and the harness now asserts the stacked bars clear a
    36px icon so the overlap cannot come back.
  • Layout reset, and per-character layouts
    Two follow-ups, both smaller than they sound because Edit Mode already has the
    machinery.
    RESET. It turned out to already exist without being named: Edit Mode saves
    your dragging into the layout itself, so rebuilding and overwriting is exactly
    "put it back how HelloUI had it". /hui layout was already doing that. Added
    Layout:Reset as an explicit alias, relabelled the options button to "Apply /
    reset the bar layout", and said so in the tooltip - the behaviour was fine, the
    discoverability was not.
    PER-CHARACTER. Edit Mode layouts carry a layoutType, and Character layouts are
    only visible to the character that owns them, so this is a choice of enum value
    plus a name rather than any new mechanism. Off by default, matching the old
    profile where 47 characters shared one arrangement; on, each character gets
    "HelloUI - <name>" typed Character, so tuning the priest stops moving the
    warrior's bars. The auto-apply latch follows the mode - account-wide latches
    once for the account, per-character once per character - so switching the
    setting gives each character its own copy as it logs in rather than doing
    nothing until asked.
    Blizzard caps layouts at 5 per type (EditModeMaxLayoutsPerType), counted
    separately for Account and Character. Checked before inserting, with an error
    naming which type is full, because SaveLayouts past the cap fails opaquely.
    Switching modes deliberately leaves the previous layout in place rather than
    deleting it - this addon does not delete a player's layouts, and one stale
    dropdown entry is a much better failure than a removed one.
    107 checks. The reset test drags a bar and resizes it in the saved layout, then
    asserts re-applying restores both; mutation-tested against typing the
    per-character layout as Account.
  • Build the DragonflightUI bar layout as an Edit Mode layout
    The de-clutter was landing correctly but the bars were in Blizzard's
    arrangement, not DragonflightUI's. That gap was invisible to the method this
    addon was scoped by: AceDB records deviations from defaults, and the layout was
    DFUI's default, so it could never appear in the saved profile. Same blind spot
    as the gryphons, one size larger.
    This does not break "delegate position to Edit Mode" - it is that rule taken
    seriously. Rather than fighting Edit Mode with SetPoint on frames it silently
    re-anchors on every layout save, spec change and Edit Mode close, HelloUI
    writes a real Edit Mode layout and lets Edit Mode apply it. Afterwards the
    addon is uninvolved: it is the player's layout, editable and persistent like
    any other.
    Non-destructive by construction. It adds a layout named HelloUI and never
    writes to an existing one, so the worst case is one extra entry in the
    dropdown. Applied once and latched on layoutAppliedV1 - the one-time-migration
    idiom the era-1159 fork used for its minimap tuck - because re-applying every
    login would undo any bar the player had since dragged, which is precisely the
    behaviour the rest of this design exists to avoid.
    Geometry is DragonflightUI's own defaults from its Actionbar.lua: buttonScale
    0.8, padding 2, bars stacked upward from the status bars, the two 3-row blocks
    flanking at +/-64. Edit Mode stores those two scalars pre-conversion
    (display = raw * stepSize + minValue), so 80% is raw 3 and 2px padding raw 0.
    Bar-to-bar anchoring rather than computed pixel offsets, because row height
    depends on icon size and Blizzard already knows it.
    Three shape traps, all found by reading the client source and all now covered
    by the harness:
    • settings is an ARRAY of {setting, value} pairs, not the map form the preset
      files use. A map write silently corrupts the array instead of erroring.
    • anchorInfo.relativeTo is a frame NAME string.
    • isInDefaultPosition must be set false on anything moved, or Edit Mode treats
      the system as untouched and re-slams it to the preset anchor.
      The systems array is built from a copy of Blizzard's own preset rather than by
      hand, so every system HelloUI does not care about carries Blizzard's correct
      entry and the layout cannot be missing one.
      99 checks. Mutation-tested against the isInDefaultPosition flag and the
      map-vs-array trap; the latter now fails cleanly rather than crashing.
  • Hide the gryphons and main bar backdrop
    The design's evidence table marked gryphons = 'NONE' and hideArt = true as
    "nothing to do - DFUI's own art". That reasoning was correct and beside the
    point: those settings were DragonflightUI declining to draw art of its own, so
    there was nothing to port, but Blizzard's gryphons and bar backdrop are right
    there on a stock client and the profile is unambiguous that they were not
    wanted. Reading the mechanism and losing sight of the intent dropped a real
    feature; the screenshots put it back on the agenda.
    Implemented as appearance rather than position, which is what makes it
    HelloUI's job at all - philosophy #2 is about anchors, and philosophy #1 is
    explicitly "keep Blizzard's frames, change their appearance". It hides the same
    two frames Blizzard's own Edit Mode HideBarArt drives - MainMenuBar and
    MainActionBar.EndCaps, per MainActionBarMixin:UpdateEndCaps - directly, rather
    than through EditModeManagerFrame:OnSystemSettingChange, which would write
    manager state in our taint context and persist into the player's saved layout.
    Hiding two unprotected art frames does neither and reverses cleanly; restoring
    asks UpdateEndCaps which of the two belongs on screen rather than guessing,
    since that depends on whether the bar has been moved.
    Checked before shipping: MicroMenuContainer and BagsBar are children of
    UIParent on this build, so the micro menu and bags survive. The latency strip
    does not - it lives inside MainMenuBar - which is exactly what Blizzard's own
    setting does too.
    The re-hide hook goes on the MainActionBar INSTANCE, not MainActionBarMixin.
    Mixin() copies functions onto the frame at creation, so hooking the mixin table
    reaches nothing - the same mistake that made the old UpdateHotkeys hook inert,
    and now mutation-tested so it cannot come back.
  • Fix nine defects found by adversarial review; drop the chat pin
    A 33-agent review against the 1.15.9 client source produced 28 findings; 20
    survived independent refutation. The seven that changed behaviour:
    • Bars force-enabled every proxy bar it was not hiding. Any bar the player had
      switched off in Blizzard's own Action Bars options came back at the next
      login - HelloUI silently overwriting a setting it does not own, in the one
      module whose whole argument is that Blizzard's setting is the right
      mechanism. It now only ever writes to turn a bar off, remembers what the
      player had in saved variables, and restores exactly that.
    • Options stored v or nil for nested checkboxes, so unticking deleted the key
      and Config's applyDefaults resurrected the default on the next ADDON_LOADED.
      Unticking "bar 5" worked for the session and was silently undone forever.
    • Friends:Refresh walked FriendsListFrame.ScrollFrame, which does not exist:
      the scroll frame is the global FriendsFrameFriendsScrollFrame, declared with
      no parentKey. The nil guard turned the whole repaint path into a no-op.
    • Friends repainted un-coloured rows with FRIENDS_WOW_NAME_COLOR to "clean up
      after recycling", which turned every offline friend blue. Blizzard sets the
      right colour before our hook runs; the fix is to leave it alone.
    • StatusBars kept the player's xpBarText in a file-local. After one /reload the
      capture was of HelloUI's own written value, so disabling the feature restored
      "1" forever. Now persisted.
    • Suppressing bar 1 left ActionBarPageNumber's page arrows invisible but fully
      clickable. Mouse state now covers the bar's child frames, recording each
      one's prior value so nothing Blizzard had deliberately disabled gets enabled.
    • /hui char barsoff reported CharOverrideList(), which is the list of overridden
      setting names - so it always said "barsOff" whatever you toggled. It also
      stored typos silently. Now validates against the bar table and reports bars.
      And the chat pin is gone. ChatFrame1 inherits EditModeChatFrameSystemTemplate
      on 1.15.9 and Edit Mode's preset carries its width and height as well as its
      anchor, so pinning it drives the same overrides, the same taint path and the
      same silent revert that made the minimap tuck unshippable. Keeping one and
      dropping the other would have made the rule decorative. Eight features now.
      Also: EnableMouse is a protected function on this build (the comment claimed
      otherwise), and .luacheckrc now declares its read_globals so luacheck . is
      clean at 0 warnings like the siblings.
      The harness grew to 80 checks with one regression per fixed bug, and now fails
      on any error the addon's SafeCall contains - a swallowed error was previously a
      silent pass. Mutation-tested against three of the fixes.
  • Drop the UpdateHotkeys hook - it could never have worked
    Two findings from Blizzard's 1.15.9 source, and together they delete code
    rather than add it.
    Blizzard never calls SetAlpha on an action button's HotKey or Name. It reaches
    both font strings through Show, Hide, SetText and SetVertexColor only; the
    whole-tree search for SetAlpha on those turns up the raid pullout buttons and
    the commentator UI and nothing else. So an alpha set once stays set, and the
    correct amount of re-assertion is zero. That is also the real argument for
    alpha over Hide - not merely that it dodges a fight, but that it is permanent.
    And the hook was inert anyway: Mixin() copies function references onto each
    button when it is created, so hooking ActionBarActionButtonMixin.UpdateHotkeys
    afterwards reaches no existing button. It would have reported itself as working
    in /hui status while doing nothing - which is worse than not having it.
    What is left is three events as insurance against a button created after the
    first pass. On Era every button exists at load, so they should never fire; they
    are cheap and the cost of being wrong is a stray keybind on screen.
  • Reconcile the design with what the client source actually said, add README
    Six factual corrections to DESIGN.md, all from Blizzard's shipping 1.15.9
    source rather than inference:
    • MINIMAP_PING was not removed. The event still exists with a documented
      payload; Blizzard moved to RegisterEventCallback and plain RegisterEvent now
      rejects it, which is what the reported error actually said.
    • Status bar text is one native cvar, not two draw-layer changes, and the two
      settings collapse into one because both bars share the predicate.
    • The player health bar needs no hook - lockColor is Blizzard handing it over.
    • The minimap feature is the time-of-day dial, and no positioning ships: stock
      is already flush top-right and MinimapCluster is an Edit Mode system.
    • Bar numbering: DragonflightUI's bar4 is Blizzard's bar 5.
    • Four siblings park a minimap button, not three - HelloLog was missing.
      The Edit Mode question the design flagged as its highest-value unknown is now
      answered in both directions: per-bar visibility IS native for bars 2-8, and
      keybind/macro text is NOT native anywhere, so one feature got simpler and one
      stayed real work.
      The TODO list is rewritten around what actually remains, and now says plainly
      at the top that none of this has been run in the game yet - the harness covers
      logic and end state, not whether a frame looks right.
  • Implement the nine features, plus an offline boot harness
    Every API decision here is grounded in Blizzard's own shipping UI source for
    this exact build: Gethe/wow-ui-source branch classic_era, version.txt
    1.15.9.68808, matching .build.info for wow_classic_era. Several things the
    design assumed turned out to be wrong, and the code follows the source
    rather than the assumption.
    Three features got materially simpler than planned:
    • StatusBars is a single CVar. StatusTrackingBarMixin:ShouldBarTextBeDisplayed
      reads GetCVarBool("xpBarText"), so Blizzard already has the exact switch.
      The other two terms in that expression are the mouseover machinery and are
      cleared on mouse-out, so they cannot be used. Both bars share the predicate,
      which collapses the old profile's two flags into one honest setting.
    • Player needs no hook at all. UnitFrameHealthBar_Update guards its colour
      write with if not statusbar.lockColor, so setting lockColor makes Blizzard
      stop resetting it.
    • Minimap ships no positioning. Stock already anchors TOPRIGHT 0,0 in both
      Edit Mode presets, and MinimapCluster is an Edit Mode system: SetPoint is
      replaced by an override that writes manager state in our taint context, the
      frame is clampedToScreen, and any anchor is reverted on layout save, spec
      change and every Edit Mode close. Also, Era loads GameTime_NoCalendar, so
      that button is the time-of-day dial and not a calendar.
      Two traps worth naming:
    • Bar numbering. DragonflightUI bound its bar4 to MultiBarLeft; Blizzard calls
      that bar 5. These ids follow Blizzard so they match the game's own options
      labels, which moves the default from bar4 to bar5 - the same physical bar.
    • Hiding bar 1 must not Hide() the frame. IsNormalActionBarState() is
      return MainActionBar:IsShown() and every multibar is gated on it, so
      hiding it drags bars 2-8 down - exactly the case this addon exists for. Bar
      1, stance and pet have no native toggle and are made invisible and
      non-interactive instead; bars 2-8 drive Blizzard's own settings proxy.
      Buttons does NOT collapse to a Blizzard checkbox - the full-tree grep for a
      keybind/macro text setting comes back empty, so that feature is real work.
      It prefers the button.HotKey/.Name parentKey aliases over the globals.
      Darkmode is an explicit allowlist and never an enumeration: four sibling
      addons park a Hello*MinimapButton on the Minimap frame, HelloWarrior and
      HelloTotems use SetDesaturated as live state, and Masque owns button art. The
      buffs and general-UI areas are dropped - the former has no stock target, the
      latter was already a no-op upstream.
      Tests/test_boot.lua stubs the API, loads all eleven files in TOC order, drives
      ADDON_LOADED through PLAYER_ENTERING_WORLD and asserts observable end state:
      64 checks. Mutation-tested against the bar-numbering trap and the lockColor
      mechanism to confirm it fails when it should.
  • Add design document and addon foundation
    HelloUI replaces DragonflightUI on Classic Era 1.15.9 with a behaviour-only
    de-clutter layer: no texture set, no replacement frames, no reimplemented
    layout engine.
    Scope was derived by reading the DragonflightUI AceDB profile across both
    accounts and 47 characters. AceDB only writes non-default values, so that
    file is the complete list of what was actually wanted - about fifteen
    entries, every one of which removes something or makes some text
    permanently visible. DESIGN.md records the evidence and the nine features
    it produced, plus the boundaries against the sibling Hello addons.
    Foundation:
    • Core.lua: ns namespace, guarded RegisterEvent (an unguarded one is what
      took DragonflightUI's minimap module down on 1.15.9), per-site error
      containment, an out-of-combat work queue for protected frames, and the
      module registry.
    • Config.lua: account-wide settings with a sparse per-character override
      list. 47 characters shared one layout and exactly one diverged, so this
      is an exception list rather than a profile manager.

This mod has no additional files